Skip to content

Thoxy67/rust-pe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rspe

Reflective PE loader library in pure Rust (no_std). Loads native and .NET assemblies from memory.

Features

  • Native PE loading (C/C++/Rust/Go...)
    • 64-bit (x86_64)
    • 32-bit (x86)
    • ARM/ARM64 relocation support
    • Architecture validation (PE bitness must match host process)
  • .NET assembly loading (C#/VB/CLR)
    • 64-bit .NET execution via CLR hosting
    • 32-bit .NET execution via CLR hosting
    • Command-line argument passing to Main(string[] args)
    • Automatic runtime version detection from PE metadata

PE Loading Pipeline

  1. Parse DOS/NT headers (PE32 and PE32+ at runtime)
  2. Map sections into allocated memory
  3. Resolve imports (name and ordinal, PE32 4-byte / PE32+ 8-byte thunks)
  4. Apply base relocations (HIGHLOW, DIR64, ARM_MOV32, THUMB_MOV32)
  5. Resolve delayed imports (DataDirectory[13], modern RVA format)
  6. Register exception handlers (RtlAddFunctionTable, x64/ARM64 only)
  7. Set per-section memory protections
  8. Invoke TLS callbacks (DLL_PROCESS_ATTACH)
  9. Execute entry point via CreateThread
  10. Cleanup (unregister exception tables, free memory)

Advanced Security Features

  • Security Cookie Initialization (__security_cookie for /GS buffer security)
  • CFG (Control Flow Guard) - Registers valid call targets via SetProcessValidCallTargets
  • SxS / Activation Context - Activates embedded manifests for GUI applications
  • Bound Import Directory invalidation

Export Table Resolution

Utility functions for resolving exports from a loaded PE image:

  • get_export_by_name(baseptr, ntheader, name) — resolve by name
  • get_export_by_ordinal(baseptr, ntheader, ordinal) — resolve by ordinal

Handles forwarded exports automatically by loading the target DLL (e.g., "NTDLL.RtlAllocateHeap" resolves to the actual function in ntdll.dll).

API Set Resolution

Automatically resolves Windows API sets (api-ms-win-*, ext-ms-win-*) to their actual host DLLs using the PEB ApiSetMap. This enables loading PEs that import from virtual API set DLLs.

Utility Functions

  • utils::detect_platform(bytes) — Detect if PE is 32-bit or 64-bit
  • utils::check_dotnet(pe) — Check if PE is a .NET assembly
  • utils::get_dotnet_version(pe) — Extract .NET runtime version string from metadata
  • utils::rva_to_file_offset(pe, rva) — Convert RVA to raw file offset

Supported Machine Types

Architecture Machine Code Bitness
i386 0x014c 32-bit
AMD64 0x8664 64-bit
IA64 0x0200 64-bit
ARM (ARMNT) 0x01C4 32-bit
ARM64 0xAA64 64-bit

Error Handling

The library uses a PeError enum for error handling:

pub enum PeError {
    InvalidPe,
    FileTooSmall,
    ArchMismatch,        // PE bitness doesn't match host process
    AllocFailed,
    InvalidNtHeader,
    ImportResolveFailed,
    SectionOutOfBounds,
    UnsupportedRelocationType,
    ThreadCreationFailed,
    VirtualProtectFailed,
    DotNetError(&'static str),
}

Cargo Features

[dependencies]
rust-pe = { version = "0.1", default-features = true }

# Or disable .NET support to reduce binary size:
rust-pe = { version = "0.1", default-features = false }
Feature Default Description
dotnet Yes Enable .NET assembly execution via CLR hosting

Usage

Basic Usage (Embedded PE)

use rust_pe::reflective_loader;

fn main() {
    let data = include_bytes!("app.exe");

    // Native PE or .NET assembly — detected automatically
    unsafe {
        reflective_loader(data).expect("failed to load PE");
    }
}

With Command-Line Arguments

use rust_pe::reflective_loader_with_args;

fn main() {
    let data = include_bytes!("app.exe");

    // Arguments are passed to native PE via PEB CommandLine patching
    // or to .NET Main(string[] args) via CLR invocation
    unsafe {
        reflective_loader_with_args(data, &["arg1", "arg2"]).expect("failed");
    }
}

Loading from File

use rust_pe::reflective_loader;
use std::fs;

fn main() {
    let data = fs::read("path/to/executable.exe").expect("failed to read file");

    unsafe {
        reflective_loader(&data).expect("failed to load PE");
    }
}

Forwarding CLI Arguments

use rust_pe::reflective_loader_with_args;

fn main() {
    let data = include_bytes!("app.exe");

    let args: Vec<String> = std::env::args().skip(1).collect();
    let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();

    unsafe {
        reflective_loader_with_args(data, &arg_refs).expect("failed");
    }
}

How Argument Passing Works

  • Native PEs: Arguments are passed by patching the PEB CommandLine, so GetCommandLineW() returns the provided arguments
  • .NET Assemblies: Arguments are passed directly to Main(string[] args) via CLR invocation

no_std Support

This library is no_std compatible (requires alloc). It uses no standard library dependencies, making it suitable for constrained environments or when minimal binary size is important.

Credits / References

Special thanks to the following individuals and projects for their contributions to this project:

  • memN0ps for providing useful winapi rust code for learning
  • trickster0 for providing many OffensiveRust code for learning

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

Reflective PE loader written in Rust. Loads and executes native and .NET PE files directly from memory.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages