├── src ├── bin │ ├── putty.exe │ ├── putty_32.exe │ └── messagebox.exe ├── main.zig ├── utils.zig └── pe.zig ├── .gitignore ├── LICENSE └── README.md /src/bin/putty.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thoxy67/zig-pe/HEAD/src/bin/putty.exe -------------------------------------------------------------------------------- /src/bin/putty_32.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thoxy67/zig-pe/HEAD/src/bin/putty_32.exe -------------------------------------------------------------------------------- /src/bin/messagebox.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thoxy67/zig-pe/HEAD/src/bin/messagebox.exe -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### VisualStudioCode ### 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | !.vscode/*.code-snippets 8 | 9 | # Local History for Visual Studio Code 10 | .history/ 11 | 12 | # Built Visual Studio Code Extensions 13 | *.vsix 14 | 15 | ### VisualStudioCode Patch ### 16 | # Ignore all local history of files 17 | .history 18 | .ionide 19 | 20 | ### zig ### 21 | # Zig programming language 22 | 23 | zig-cache/ 24 | .zig-cache/ 25 | zig-out/ 26 | build/ 27 | build-*/ 28 | docgen_tmp/ 29 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const pe = @import("pe.zig"); 3 | 4 | pub fn main() !void { 5 | 6 | // Use local PE 7 | // var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 8 | // defer _ = gpa.deinit(); 9 | // const allocator = gpa.allocator(); 10 | // const file_name = "src/bin/putty.exe"; 11 | // const file_content = try std.fs.cwd().readFileAlloc(allocator, file_name, std.math.maxInt(usize)); 12 | // defer allocator.free(file_content); 13 | // try pe.RunPE.init(file_content).run(); 14 | 15 | // Use embed PE 16 | try pe.RunPE.init(@embedFile("bin/putty.exe")).run(); 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Thoxy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/utils.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const win = @cImport(@cInclude("windows.h")); 3 | const builtin = @import("builtin"); 4 | 5 | /// Check if the PE file is a .NET assembly 6 | pub fn is_dotnet_assembly(nt_headers: *win.IMAGE_NT_HEADERS) bool { 7 | const data_directory = &nt_headers.OptionalHeader.DataDirectory[win.IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR]; 8 | return data_directory.VirtualAddress != 0; 9 | } 10 | 11 | /// Detect if the target pe platform is 32 or 64bit 12 | pub fn detect_platform(bytes: []const u8) !u32 { 13 | const pe_offset = std.mem.readVarInt(u32, bytes[0x3C..0x40], .little); 14 | if (bytes.len < pe_offset + 6) return error.InvalidMachineTypePE; 15 | const machine = std.mem.readVarInt(u16, bytes[pe_offset + 4 .. pe_offset + 6], .little); 16 | return switch (machine) { 17 | 0x014c => 32, // IMAGE_FILE_MACHINE_I386 18 | 0x0200 => 64, // IMAGE_FILE_MACHINE_IA64 19 | 0x8664 => 64, // IMAGE_FILE_MACHINE_AMD64 20 | else => error.NotSupportedPlatform, 21 | }; 22 | } 23 | 24 | /// Wait for the created thread to complete execution 25 | pub fn waitForThreadCompletion(thread_handle: win.HANDLE) !win.DWORD { 26 | const wait_result = std.os.windows.kernel32.WaitForSingleObject(thread_handle.?, std.os.windows.INFINITE); 27 | switch (wait_result) { 28 | std.os.windows.WAIT_OBJECT_0 => {}, 29 | std.os.windows.WAIT_TIMEOUT => {}, 30 | std.os.windows.WAIT_FAILED => return error.WaitFailed, 31 | else => return error.UnexpectedWaitResult, 32 | } 33 | var exit_code: win.DWORD = undefined; 34 | return if (win.GetExitCodeThread(thread_handle, &exit_code) == 0) error.GetExitCodeFailed else exit_code; 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-pe 2 | 3 | A simple RunPE loader written in Zig, designed to load and execute Portable Executable (PE) files in memory. 4 | 5 | ## Overview 6 | 7 | This project implements a RunPE loader in Zig, allowing for the dynamic loading and execution of PE files directly from memory. It's designed to work with native compiled binaries and provides a set of functions to handle various aspects of PE file manipulation. 8 | 9 | ## Features 10 | 11 | - [x] Parse and retrieve DOS_IMAGE_HEADER 12 | - [x] Parse and retrieve NT_IMAGE_HEADER 13 | - [x] Calculate DOS_IMAGE_HEADER size 14 | - [x] Calculate NT_IMAGE_HEADER size 15 | - [x] Allocate memory for PE image binding 16 | - [x] Copy PE file contents to allocated memory 17 | - [x] Write sections to header 18 | - [x] Handle import table (load required DLLs and resolve functions) 19 | - [x] Fix base relocations 20 | - [x] Change Memory Protection 21 | - [x] Execute the PE file's entry point 22 | 23 | ## Compatibility 24 | 25 | - [x] Native compiled binary execution 26 | - [ ] .NET compiled binary execution (not yet implemented) 27 | 28 | ## Prerequisites 29 | 30 | - Zig compiler (latest version recommended) 31 | - Windows OS (the project uses Windows-specific APIs) 32 | 33 | ## Building the Project 34 | 35 | 1. Clone the repository: 36 | ``` 37 | git clone https://github.com/yourusername/zig-pe.git 38 | cd zig-pe 39 | ``` 40 | 41 | 2. Build the project: 42 | ``` 43 | zig build 44 | ``` 45 | 46 | ## Usage 47 | 48 | Here's a basic example of how to use the zig-pe loader: 49 | 50 | ```zig 51 | const std = @import("std"); 52 | const pe = @import("pe.zig"); 53 | 54 | pub fn main() !void { 55 | 56 | // Use local PE 57 | // var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 58 | // defer _ = gpa.deinit(); 59 | // const allocator = gpa.allocator(); 60 | // const file_name = "src/bin/putty.exe"; 61 | // const file_content = try std.fs.cwd().readFileAlloc(allocator, file_name, std.math.maxInt(usize)); 62 | // defer allocator.free(file_content); 63 | 64 | // Use embed PE 65 | try pe.RunPE.init(@embedFile("bin/putty.exe")).run(); 66 | } 67 | 68 | ``` 69 | 70 | ## Security Considerations 71 | 72 | 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. 73 | 74 | ## Contributing 75 | 76 | Contributions to zig-pe are welcome! Please feel free to submit pull requests, create issues or spread the word. 77 | 78 | 1. Fork the Project 79 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 80 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 81 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 82 | 5. Open a Pull Request 83 | 84 | ## License 85 | 86 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 87 | 88 | ## Acknowledgments 89 | 90 | - The Zig programming language community 91 | - Contributors to PE file format documentation 92 | 93 | ## Disclaimer 94 | 95 | This project is for educational purposes only. Ensure you have the necessary rights and permissions before loading and executing any PE file. 96 | -------------------------------------------------------------------------------- /src/pe.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const utils = @import("utils.zig"); 3 | const win = @cImport(@cInclude("windows.h")); 4 | 5 | pub const RunPE = struct { 6 | buffer: []const u8, 7 | addr_alloc: ?*anyopaque, 8 | addr_array_ptr: [*]u8, 9 | dosheader: *win.IMAGE_DOS_HEADER, 10 | ntheaders: *win.IMAGE_NT_HEADERS, 11 | platform: ?u32, 12 | is_32bit: bool, 13 | 14 | /// Initialize the RunPE struct with the given buffer 15 | pub fn init(buffer: []const u8) *RunPE { 16 | var pe = RunPE{ 17 | .buffer = buffer, 18 | .addr_alloc = undefined, 19 | .addr_array_ptr = undefined, 20 | .dosheader = undefined, 21 | .ntheaders = undefined, 22 | .platform = null, 23 | .is_32bit = false, 24 | }; 25 | 26 | return &pe; 27 | } 28 | 29 | /// Get the size of the PE headers 30 | fn get_headers_size(self: *RunPE) usize { 31 | const e_lfanew: u32 = std.mem.readVarInt(u32, self.buffer[60..64], std.builtin.Endian.little); 32 | return std.mem.readVarInt(u32, self.buffer[e_lfanew + 24 + 60 .. e_lfanew + 24 + 60 + 4], std.builtin.Endian.little); 33 | } 34 | 35 | /// Get the size of the PE image 36 | fn get_image_size(self: *RunPE) usize { 37 | const e_lfanew: u32 = std.mem.readVarInt(u32, self.buffer[60..64], std.builtin.Endian.little); 38 | return std.mem.readVarInt(u32, self.buffer[e_lfanew + 24 + 56 .. e_lfanew + 24 + 60], std.builtin.Endian.little); 39 | } 40 | 41 | /// Get the DOS header of the PE file 42 | pub fn get_dos_header(self: *RunPE) !void { 43 | self.dosheader = @ptrCast(@alignCast(self.addr_array_ptr)); 44 | if (self.dosheader.e_magic != 0x5A4D) return error.InvalidDOSHeader; 45 | } 46 | 47 | /// Get the NT header of the PE file 48 | pub fn get_nt_header(self: *RunPE) !void { 49 | self.platform = try utils.detect_platform(self.buffer); 50 | if (self.platform == 32) { 51 | self.is_32bit = true; 52 | } 53 | 54 | self.ntheaders = @ptrFromInt(@intFromPtr(self.addr_array_ptr) + @as(usize, @intCast(self.dosheader.e_lfanew))); 55 | 56 | if (self.ntheaders.Signature != 0x00004550) return error.InvalidNTHeader; 57 | } 58 | 59 | /// Allocate memory for the PE image 60 | pub fn allocateMemory(self: *RunPE) !void { 61 | self.addr_alloc = try std.os.windows.VirtualAlloc(null, self.get_image_size(), std.os.windows.MEM_COMMIT | std.os.windows.MEM_RESERVE, std.os.windows.PAGE_READWRITE); 62 | self.addr_array_ptr = @ptrCast(self.addr_alloc); 63 | } 64 | 65 | /// Copy the PE headers to the allocated memory 66 | pub fn copyHeaders(self: *RunPE) !void { 67 | const header_size = self.get_headers_size(); 68 | @memcpy(self.addr_array_ptr[0..header_size], self.buffer[0..header_size]); 69 | try self.get_dos_header(); 70 | } 71 | 72 | /// Write each section of the PE file to the allocated memory 73 | fn write_sections(self: *RunPE) !void { 74 | const section_header_offset = @intFromPtr(self.addr_array_ptr) + @as(usize, @intCast(self.dosheader.e_lfanew)) + @sizeOf(win.IMAGE_NT_HEADERS); 75 | for (0..self.ntheaders.FileHeader.NumberOfSections) |count| { 76 | const nt_section_header: *win.IMAGE_SECTION_HEADER = @ptrFromInt(section_header_offset + (count * @sizeOf(win.IMAGE_SECTION_HEADER))); 77 | if (nt_section_header.PointerToRawData == 0 or nt_section_header.SizeOfRawData == 0) continue; 78 | if (nt_section_header.PointerToRawData + nt_section_header.SizeOfRawData > self.buffer.len) return error.SectionOutOfBounds; 79 | const src = self.buffer[nt_section_header.PointerToRawData..][0..nt_section_header.SizeOfRawData]; 80 | @memcpy((self.addr_array_ptr + nt_section_header.VirtualAddress)[0..src.len], src); 81 | } 82 | } 83 | 84 | /// Write the import table of the PE file to the allocated memory 85 | fn write_import_table(self: *RunPE) !void { 86 | if (self.ntheaders.OptionalHeader.DataDirectory[win.IMAGE_DIRECTORY_ENTRY_IMPORT].Size == 0) return; 87 | var importDescriptorPtr: *win.IMAGE_IMPORT_DESCRIPTOR = @ptrFromInt(@intFromPtr(self.addr_array_ptr) + @as(usize, @intCast(self.ntheaders.OptionalHeader.DataDirectory[win.IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress))); 88 | while (importDescriptorPtr.Name != 0 and importDescriptorPtr.FirstThunk != 0) : (importDescriptorPtr = @ptrFromInt(@intFromPtr(importDescriptorPtr) + @sizeOf(win.IMAGE_IMPORT_DESCRIPTOR))) { 89 | const dll_handle: win.HMODULE = win.LoadLibraryA(std.mem.sliceTo(@as([*:0]const u8, @ptrFromInt(@intFromPtr(self.addr_array_ptr) + @as(usize, @intCast(importDescriptorPtr.Name)))), 0).ptr) orelse return error.ImportResolutionFailed; 90 | defer std.os.windows.FreeLibrary(@ptrCast(dll_handle.?)); 91 | 92 | var thunk: *align(1) usize = @ptrFromInt(@intFromPtr(self.addr_array_ptr) + importDescriptorPtr.FirstThunk); 93 | while (thunk.* != 0) : (thunk = @ptrFromInt(@intFromPtr(thunk) + @sizeOf(usize))) { 94 | thunk.* = if (thunk.* & (1 << 63) != 0) 95 | @intFromPtr(std.os.windows.kernel32.GetProcAddress(@ptrCast(dll_handle), @ptrFromInt(@as(usize, @as(u16, @truncate(thunk.* & 0xFFFF)))))) 96 | else 97 | @intFromPtr(std.os.windows.kernel32.GetProcAddress(@ptrCast(dll_handle), @ptrCast(&@as(*align(1) const win.IMAGE_IMPORT_BY_NAME, @ptrFromInt(@intFromPtr(self.addr_array_ptr) + thunk.*)).Name[0]))); 98 | 99 | if (thunk.* == 0) return error.ImportResolutionFailed; 100 | } 101 | } 102 | } 103 | 104 | /// Fix PE base relocations 105 | fn fix_base_relocations(self: *RunPE) !void { 106 | if (self.ntheaders.OptionalHeader.DataDirectory[win.IMAGE_DIRECTORY_ENTRY_BASERELOC].Size == 0) return; 107 | var reloc_block: *win.IMAGE_BASE_RELOCATION = @ptrCast(@alignCast(self.addr_array_ptr + self.ntheaders.OptionalHeader.DataDirectory[win.IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress)); 108 | while (reloc_block.SizeOfBlock != 0) : (reloc_block = @ptrFromInt(@intFromPtr(reloc_block) + reloc_block.SizeOfBlock)) { 109 | for (@as([*]u16, @ptrCast(@alignCast(@as([*]u8, @ptrCast(reloc_block)) + @sizeOf(win.IMAGE_BASE_RELOCATION))))[0 .. (reloc_block.SizeOfBlock - @sizeOf(win.IMAGE_BASE_RELOCATION)) / 2]) |entry| { 110 | const offset = entry & 0xFFF; 111 | switch (entry >> 12) { 112 | win.IMAGE_REL_BASED_HIGHLOW => @as(*u32, @ptrCast(@alignCast(self.addr_array_ptr + reloc_block.VirtualAddress + offset))).* +%= @truncate(@intFromPtr(self.addr_array_ptr) - self.ntheaders.OptionalHeader.ImageBase), 113 | win.IMAGE_REL_BASED_DIR64 => @as(*usize, @ptrCast(@alignCast(self.addr_array_ptr + reloc_block.VirtualAddress + offset))).* +%= @as(usize, @bitCast(@intFromPtr(self.addr_array_ptr) - self.ntheaders.OptionalHeader.ImageBase)), 114 | win.IMAGE_REL_BASED_ABSOLUTE => {}, 115 | else => return error.UnsupportedRelocationType, 116 | } 117 | } 118 | } 119 | } 120 | 121 | /// Change memory protection for PE sections 122 | fn changeMemoryProtection(self: *RunPE) !void { 123 | var old_protect: std.os.windows.DWORD = undefined; 124 | const dos_header = @as(*win.IMAGE_DOS_HEADER, @ptrCast(@alignCast(self.addr_array_ptr))); 125 | const section_header_offset = @intFromPtr(self.addr_array_ptr) + @as(usize, @intCast(dos_header.e_lfanew)) + @sizeOf(win.IMAGE_NT_HEADERS); 126 | for (0..self.ntheaders.FileHeader.NumberOfSections) |i| { 127 | const section: *win.IMAGE_SECTION_HEADER = @ptrFromInt(section_header_offset + (i * @sizeOf(win.IMAGE_SECTION_HEADER))); 128 | var new_protect: std.os.windows.DWORD = std.os.windows.PAGE_READONLY; 129 | if (section.Characteristics & win.IMAGE_SCN_MEM_EXECUTE != 0) new_protect = std.os.windows.PAGE_EXECUTE_READ; 130 | if (section.Characteristics & win.IMAGE_SCN_MEM_WRITE != 0) new_protect = std.os.windows.PAGE_READWRITE; 131 | if (section.Characteristics & win.IMAGE_SCN_MEM_EXECUTE != 0 and section.Characteristics & win.IMAGE_SCN_MEM_WRITE != 0) new_protect = std.os.windows.PAGE_EXECUTE_READWRITE; 132 | try std.os.windows.VirtualProtect(self.addr_array_ptr + section.VirtualAddress, section.Misc.VirtualSize, new_protect, &old_protect); 133 | } 134 | } 135 | 136 | /// Create and run a new thread for the loaded PE 137 | fn createAndRunThread(self: *RunPE, nt_header: *win.IMAGE_NT_HEADERS) !win.HANDLE { 138 | const thread_handle = std.os.windows.kernel32.CreateThread(null, 0, @as(std.os.windows.LPTHREAD_START_ROUTINE, @ptrCast(@alignCast(@as(*const fn () callconv(.C) void, @ptrFromInt(@intFromPtr(self.addr_array_ptr) + nt_header.OptionalHeader.AddressOfEntryPoint))))), null, 0, null); 139 | if (thread_handle == null) return error.ThreadCreationFailed; 140 | return thread_handle.?; 141 | } 142 | 143 | /// Execute the loaded PE file 144 | fn executeLoadedPE( 145 | self: *RunPE, 146 | ) !void { 147 | const thread_handle = try self.createAndRunThread(self.ntheaders); 148 | defer std.os.windows.CloseHandle(thread_handle.?); 149 | _ = try utils.waitForThreadCompletion(thread_handle); 150 | } 151 | 152 | /// Main function to run the PE file 153 | pub fn run(self: *RunPE) !void { 154 | try self.allocateMemory(); 155 | defer _ = std.os.windows.VirtualFree(self.addr_alloc, 0, std.os.windows.MEM_RELEASE); 156 | try self.copyHeaders(); 157 | 158 | try self.get_nt_header(); 159 | 160 | if (!utils.is_dotnet_assembly(self.ntheaders)) { 161 | try self.write_sections(); 162 | try self.write_import_table(); 163 | try self.fix_base_relocations(); 164 | try self.changeMemoryProtection(); 165 | try self.executeLoadedPE(); 166 | } else { 167 | return error.UnsuportedDotNET; 168 | } 169 | } 170 | }; 171 | --------------------------------------------------------------------------------