PERUN is a lightweight, security-hardened Windows PE32+ (64-bit AMD64) console executable loader and runtime library designed to run natively on Linux x86_64.
Written strictly using the C99 standard and POSIX APIs, PERUN handles PE headers parsing, virtual memory mapping, page-level permission configuration, and base relocation patching with rigorous boundary checking to mitigate common binary parsing vulnerabilities.
- DOS Header Validation: Verifies the
MZ (0x5A4D)magic number at the beginning of the file. - NT Header Validation: Sanitizes the
e_lfanewoffset to prevent out-of-bounds memory access before validating thePE\0\0 (0x00004550)signature. - Architecture Verification: Ensures the target machine type is
AMD64 (0x8664)and the optional header magic isPE32+ (0x20b), ensuring x64 console executable compatibility. - Section Bounds Checking: Validates the layout of section headers against the raw file size to prevent malformed PE files from causing parser crashes.
- Memory Allocation: Maps a virtual memory block matching
SizeOfImageviammap(initially with read-write permissions:PROT_READ | PROT_WRITE). - Headers & Sections Copy: Copies the PE headers up to
SizeOfHeadersto the base address and maps each section to its target Relative Virtual Address (RVA). - BSS Padding: Safely zero-fills any remaining virtual memory space if the
VirtualSizeof a section is greater than itsSizeOfRawData. - Memory Protection (
mprotect): Maps section characteristics (Read/Write/Execute) to Linux page protection flags. Page-aligns the target addresses and sizes usingsysconf(_SC_PAGESIZE)to safely invokemprotect.
- Delta Computation: Computes the address difference between the mapped base address (
loaded_base) and the compiler's preferred base address (ImageBase). - Relocation Directory Parsing: References the
IMAGE_DIRECTORY_ENTRY_BASERELOCto parse relocation blocks (IMAGE_BASE_RELOCATION) page-by-page. - 64-bit Address Patching: Decodes 16-bit relocation entries and adds the computed delta to absolute 64-bit pointers (
IMAGE_REL_BASED_DIR64). - Out-of-Bounds (OOB) Defense: Performs strict boundary verification on block sizes, block counts, and target RVAs to prevent arbitrary memory write vulnerabilities.
- Import Directory Parsing: Navigates the
IMAGE_DIRECTORY_ENTRY_IMPORTdata directory to traverse the DLL import descriptors and lookup tables (ILT). - Mock Stub Engine: Integrates native C mock stubs (e.g.,
mock_ExitProcess,mock_printf) to intercept and handle calls intended for Windows system libraries on Linux. - IAT Binding: Patches the Import Address Table (IAT) with the actual addresses of the resolved mock functions in memory before execution.
- ABI Calling Convention Bridge: Interposes the Microsoft x64 calling convention (
ms_abi) for all mock function definitions, resolving register mismatches (RCXvsRDI) between PE binary execution and Linux host execution. - ISO C Compliant Execution: Transitions execution control to the mapped PE
AddressOfEntryPointusing ISO C compliant union type casts to bypass-Wpedanticwarnings.
- Format String Protection: Ensures error logging and console reporting functions (
printf,fprintf) always use static format strings, protecting the application against format string exploits from input filenames. - UAF & Double-Free Mitigation: Immediately assigns
NULLto file streams and memory buffers after closing or freeing them (fclose,free) to prevent Use-After-Free (UAF) and Double-Free vectors.
PERUN/
├── CMakeLists.txt # CMake build configuration
├── README.md # Project documentation (This file)
├── .gitignore # Git ignore rules
├── include/ # Header files
│ ├── pe_format.h # PE32+ structure definitions and macros
│ ├── pe_parser.h # PE parser context and function declarations
│ ├── pe_loader.h # PE loader/mapper/relocator interface
│ └── perun.h # Common perun context structure
├── src/ # Source files
│ ├── main.c # Program entry point and flow orchestration
│ ├── pe_parser.c # PE parser implementation (Phase 1)
│ └── pe_loader.c # PE mapping, protection, relocation engine (Phase 2 & 3)
└── tests/ # Testing directory
├── generate_dummy_pe.py # Script to generate a dummy PE32+ binary with relocation table
└── dummy.exe # Synthesized target binary for loading tests
To build the project on a Linux x86_64 system, make sure CMake and make are installed:
# 1. Create and enter a build directory
mkdir build
cd build
# 2. Generate CMake build files
cmake ..
# 3. Compile the executable
makeUpon a successful build, the build/perun binary will be created.
Print essential DOS/NT header metadata and the list of sections in a readable format:
./build/perun --info tests/dummy.exeExample Output:
Machine: x86_64
Format: PE32+
ImageBase: 0x140000000
EntryPoint RVA: 0x1000
Number of sections: 4
Sections:
.text RVA=0x1000 RAW=0x200 SIZE=0x200
.data RVA=0x2000 RAW=0x400 SIZE=0x100
.reloc RVA=0x3000 RAW=0x600 SIZE=0x200
.idata RVA=0x4000 RAW=0x800 SIZE=0x200
Simulate mapping, applying base relocation, binding resolved IAT functions, and executing the entry point of the PE:
./build/perun tests/dummy.exeExample Output:
Applying Relocation (Delta: 0x7069f6244000)...
Successfully applied relocations.
Resolving Imports...
Importing from DLL: kernel32.dll
[Function] ExitProcess -> Resolved to mock_ExitProcess (0x57a1cde8783e)
Successfully resolved all imports.
Successfully mapped and loaded PE in memory!
Allocated Base VA: 0x706b36244000
SizeOfImage: 0x5000
Mapped Sections:
.text RVA=0x1000 -> Memory VA=0x706b36245000 (Size=0x200 )
-> [Debug] Value at .text+0x10 (relocated ptr): 0x706b36246000 (expected to match .data VA)
.data RVA=0x2000 -> Memory VA=0x706b36246000 (Size=0x100 )
.reloc RVA=0x3000 -> Memory VA=0x706b36247000 (Size=0x200 )
.idata RVA=0x4000 -> Memory VA=0x706b36248000 (Size=0x200 )
-> [Debug] Value at .idata+0x40 (resolved IAT entry): 0x57a1cde8783e
Starting execution flow...
Executing Entry Point (VA: 0x706b36245000)...
[Mock API] ExitProcess called with code: 42
-
Setup: The testing script (
generate_dummy_pe.py) injects a 64-bit absolute pointer0x140002000(ImageBase0x140000000+.datasection RVA0x2000) into the.textsection at offset0x10. -
Delta Computation: If the loader maps the image at address
0x706b36244000, the delta offset is computed as:$$\text{Delta} = 0x706b36244000 - 0x140000000 = 0x7069f6244000$$ -
Patch Application: The relocation engine adds this delta to the absolute pointer at
.textoffset0x10:$$\text{Patched Value} = 0x140002000 + 0x7069f6244000 = 0x706b36246000$$ -
Verification: The patched value (
0x706b36246000) matches the actual absolute virtual memory address where the.datasection was mapped (Memory VA = 0x706b36246000).
- Objective: Allow C-based mock functions compiled for System V ABI (Linux x64) to receive parameters properly from Windows x64 binaries calling them.
- Solution: We apply
__attribute__((ms_abi))to all mock function interfaces (e.g.mock_ExitProcess). - Execution Flow:
- The entry point of
dummy.exeexecutes code that sets up parameters under Microsoft x64 calling convention:- Sets the first parameter
rcxto42.
- Sets the first parameter
- The code loads the
ExitProcessaddress from the resolved IAT at0x4040(value0x57a1cde8783e) and performs a call. - The control transitions to
mock_ExitProcess. The function parsesrcxas the exit code parameter, logsExitProcess called with code: 42, and callsexit(42). - The program successfully exits with code
42, proving that IAT resolution, memory protections, and execution flow are correctly mapped.
- The entry point of