using System.ComponentModel; using System.Runtime.InteropServices; using WhiteMagic.Native; namespace WhiteMagic.Discovery; /// /// Represents a section in a PE file. /// public readonly record struct PeSection { /// /// The 8-byte null-terminated section name (e.g., ".text", ".data"). /// public string Name { get; init; } /// /// The virtual address of the section when loaded into memory (RVA). /// public IntPtr VirtualAddress { get; init; } /// /// The size of the section in memory. /// public int VirtualSize { get; init; } } /// /// Parses PE headers to expose section information and entry points. /// public sealed class PeHeaderParser { private readonly MemoryBase _memory; private readonly IntPtr _baseAddress; /// /// Creates a new PE header parser for the module at the specified base address. /// /// The memory accessor. /// The base address of the module. public PeHeaderParser(MemoryBase memory, IntPtr baseAddress) { ArgumentNullException.ThrowIfNull(memory); if (baseAddress == IntPtr.Zero) throw new ArgumentException("Base address cannot be zero.", nameof(baseAddress)); _memory = memory; _baseAddress = baseAddress; } /// /// Gets the entry point RVA (Relative Virtual Address) of the PE file. /// /// The entry point RVA, or if unavailable. /// Reading memory fails. /// The PE headers are invalid. public IntPtr EntryPoint { get { // Read and parse PE headers var (optionalHeader, _) = ParseOptionalHeader(); if (optionalHeader is null) return IntPtr.Zero; // Entry point RVA is at offset 16 in the optional header (both PE32 and PE32+) return (IntPtr)BitConverter.ToUInt32(optionalHeader.AsSpan(16, 4)); } } /// /// Enumerates all sections in the PE file. /// /// An enumerable of PE sections. /// Reading memory fails. /// The PE headers are invalid. public IEnumerable Sections { get { var (optionalHeader, sectionHeaders) = ParseOptionalHeaderAndSectionHeaders(); if (sectionHeaders is null || sectionHeaders.Length == 0) yield break; foreach (var sectionHeader in sectionHeaders) { // Parse section name (8-byte, null-terminated) string name = ParseSectionName(sectionHeader); // VirtualAddress and VirtualSize uint virtualAddress = BitConverter.ToUInt32(sectionHeader, 12); uint virtualSize = BitConverter.ToUInt32(sectionHeader, 8); yield return new PeSection { Name = name, VirtualAddress = (IntPtr)virtualAddress, VirtualSize = (int)virtualSize }; } } } /// /// Parses the DOS header, PE signature, and optional header. /// private (byte[]? OptionalHeader, byte[][]? SectionHeaders) ParseOptionalHeaderAndSectionHeaders() { // Read DOS header (first 64 bytes) byte[] dosHeader = _memory.ReadBytes(_baseAddress, 64); if (dosHeader.Length < 64) throw new InvalidDataException("Failed to read DOS header."); // Verify DOS signature "MZ" if (dosHeader[0] != 0x4D || dosHeader[1] != 0x5A) throw new InvalidDataException("Invalid DOS signature (not a PE file)."); // PE header offset is at 0x3C in DOS header int peOffset = BitConverter.ToInt32(dosHeader, 0x3C); if (peOffset < 0 || peOffset > 0x1000) // Sanity check throw new InvalidDataException($"Invalid PE offset: {peOffset}"); // Read PE signature (4 bytes: "PE\0\0") IntPtr peSigAddr = _baseAddress + peOffset; byte[] peSignature = _memory.ReadBytes(peSigAddr, 4); if (peSignature.Length < 4) throw new InvalidDataException("Failed to read PE signature."); if (peSignature[0] != 0x50 || peSignature[1] != 0x45 || peSignature[2] != 0x00 || peSignature[3] != 0x00) throw new InvalidDataException("Invalid PE signature."); // COFF header follows PE signature (20 bytes) IntPtr coffAddr = peSigAddr + 4; byte[] coffHeader = _memory.ReadBytes(coffAddr, 20); if (coffHeader.Length < 20) throw new InvalidDataException("Failed to read COFF header."); // SizeOfOptionalHeader is at offset 16 in COFF header ushort sizeOfOptionalHeader = BitConverter.ToUInt16(coffHeader, 16); // NumberOfSections is at offset 2 in COFF header ushort numberOfSections = BitConverter.ToUInt16(coffHeader, 2); if (numberOfSections == 0 || numberOfSections > 96) return (null, null); // No sections or unreasonable number // Optional header follows COFF header IntPtr optAddr = coffAddr + 20; byte[] optionalHeader = _memory.ReadBytes(optAddr, sizeOfOptionalHeader); if (optionalHeader.Length < sizeOfOptionalHeader) throw new InvalidDataException("Failed to read optional header."); // Section headers follow optional header IntPtr sectionAddr = optAddr + sizeOfOptionalHeader; int sectionHeaderSize = 40; // IMAGE_SECTION_HEADER is 40 bytes byte[][] sectionHeaders = new byte[numberOfSections][]; for (int i = 0; i < numberOfSections; i++) { byte[] section = _memory.ReadBytes(sectionAddr + (i * sectionHeaderSize), sectionHeaderSize); if (section.Length < sectionHeaderSize) throw new InvalidDataException($"Failed to read section header {i}."); sectionHeaders[i] = section; } return (optionalHeader, sectionHeaders); } /// /// Parses just the optional header (for entry point). /// private (byte[]? OptionalHeader, byte[][]? SectionHeaders) ParseOptionalHeader() { // Read DOS header (first 64 bytes) byte[] dosHeader = _memory.ReadBytes(_baseAddress, 64); if (dosHeader.Length < 64) throw new InvalidDataException("Failed to read DOS header."); // Verify DOS signature "MZ" if (dosHeader[0] != 0x4D || dosHeader[1] != 0x5A) throw new InvalidDataException("Invalid DOS signature (not a PE file)."); // PE header offset is at 0x3C in DOS header int peOffset = BitConverter.ToInt32(dosHeader, 0x3C); if (peOffset < 0 || peOffset > 0x1000) // Sanity check throw new InvalidDataException($"Invalid PE offset: {peOffset}"); // Read PE signature (4 bytes: "PE\0\0") IntPtr peSigAddr = _baseAddress + peOffset; byte[] peSignature = _memory.ReadBytes(peSigAddr, 4); if (peSignature.Length < 4) throw new InvalidDataException("Failed to read PE signature."); if (peSignature[0] != 0x50 || peSignature[1] != 0x45 || peSignature[2] != 0x00 || peSignature[3] != 0x00) throw new InvalidDataException("Invalid PE signature."); // COFF header follows PE signature (20 bytes) IntPtr coffAddr = peSigAddr + 4; byte[] coffHeader = _memory.ReadBytes(coffAddr, 20); if (coffHeader.Length < 20) throw new InvalidDataException("Failed to read COFF header."); // SizeOfOptionalHeader is at offset 16 in COFF header ushort sizeOfOptionalHeader = BitConverter.ToUInt16(coffHeader, 16); // Optional header follows COFF header IntPtr optAddr = coffAddr + 20; byte[] optionalHeader = _memory.ReadBytes(optAddr, sizeOfOptionalHeader); if (optionalHeader.Length < sizeOfOptionalHeader) throw new InvalidDataException("Failed to read optional header."); return (optionalHeader, null); } /// /// Determines whether the PE file is PE32+ (64-bit) or PE32 (32-bit). /// private bool IsPe32Plus() { var (optionalHeader, _) = ParseOptionalHeaderAndSectionHeaders(); if (optionalHeader is null || optionalHeader.Length < 2) throw new InvalidDataException("Optional header too short."); // Magic is at offset 0 in optional header // 0x10b = PE32 (32-bit), 0x20b = PE32+ (64-bit) ushort magic = BitConverter.ToUInt16(optionalHeader, 0); return magic == 0x20b; } /// /// Parses a null-terminated 8-byte section name. /// private static string ParseSectionName(byte[] sectionHeader) { // Name is first 8 bytes var nameBytes = new Span(sectionHeader, 0, 8); // Find null terminator int len = 0; for (; len < 8; len++) { if (nameBytes[len] == 0) break; } return System.Text.Encoding.ASCII.GetString(nameBytes[..len]); } }