Reflective PE loader written in Zig. Loads and executes native and .NET PE files directly from memory.
- Parse DOS and NT headers (PE32 and PE32+ at runtime)
- Map sections into allocated memory
- Resolve imports (name and ordinal, PE32 4-byte / PE32+ 8-byte thunks)
- Apply base relocations (HIGHLOW, DIR64, ARM_MOV32, THUMB_MOV32)
- Resolve delayed imports (DataDirectory[13], modern RVA format)
- Register exception handlers (RtlAddFunctionTable, x64/ARM64 only)
- Set per-section memory protections
- Invoke TLS callbacks (DLL_PROCESS_ATTACH)
- Execute entry point via CreateThread
- Cleanup (unregister exception tables, free memory)
- Security Cookie Initialization (
__security_cookiefor /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
Utility functions for resolving exports from a loaded PE image:
getExportByName(base, ntheaders, name)— resolve by namegetExportByOrdinal(base, ntheaders, 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).
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.
utils.detect_platform(buffer)— Detect if PE is 32-bit or 64-bitutils.is_dotnet_assembly(ntheaders)— Check if PE is a .NET assemblyutils.getDotNetVersion(buffer)— Extract .NET runtime version string from metadatautils.rvaToFileOffset(buffer, rva)— Convert RVA to raw file offset
- Native compiled binary execution (x86, x86_64, ARM, ARM64)
- .NET compiled binary execution via CLR hosting
- Command-line argument passing to both native and .NET executables
- Architecture validation (PE bitness must match host process)
| 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 |
- Zig compiler (latest version recommended)
- Windows OS (the project uses Windows-specific APIs)
-
Clone the repository:
git clone https://github.com/Thoxy67/zig-pe.git cd zig-pe -
Build the project:
zig build
zig build -Ddotnet=false # Disable .NET support (enabled by default)zig build # Build all targets
zig build run-putty64 # Run 64-bit native PE example
zig build run-putty32 # Run 32-bit native PE example
zig build run-dotnet # Run .NET assembly example
zig build test # Run unit testsconst pe = @import("pe");
pub fn main() !void {
// Load and execute an embedded PE file
try pe.RunPE.init(@embedFile("bin/app.exe")).run();
}const pe = @import("pe");
pub fn main() !void {
var loader = pe.RunPE.init(@embedFile("bin/app.exe"));
try loader.runWithArgs(&.{ "arg1", "arg2", "arg3" });
}const std = @import("std");
const pe = @import("pe");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const file_content = try std.fs.cwd().readFileAlloc(
allocator,
"path/to/executable.exe",
std.math.maxInt(usize),
);
defer allocator.free(file_content);
try pe.RunPE.init(file_content).run();
}const pe = @import("pe");
// After loading a DLL into memory...
const base: [*]const u8 = @ptrCast(loaded_base);
const ntheaders = // ... get NT headers
// Resolve by name
if (pe.getExportByName(base, ntheaders, "ExportedFunction")) |func_ptr| {
const func: *const fn () void = @ptrCast(@alignCast(func_ptr));
func();
}
// Resolve by ordinal
if (pe.getExportByOrdinal(base, ntheaders, 42)) |func_ptr| {
// Use the function pointer
}- 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
This project involves loading and executing arbitrary code, which can be potentially dangerous. Use this loader only with trusted PE files and in controlled environments. The authors are not responsible for any misuse or damage caused by this software.
Contributions to zig-pe are welcome! Please feel free to submit pull requests, create issues or spread the word.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- The Zig programming language community
- Contributors to PE file format documentation
This project is for educational purposes only. Ensure you have the necessary rights and permissions before loading and executing any PE file.