Implement core diagnostic memory layer, execution helpers, and high-level facade slices
Implemented: - Core: UTF-16 ReadString boundary/alignment fix, target bitness and process id on MemoryBase - function interception: PatchManager, DetourManager, InstructionAnalyzer, MainThreadDispatcher - Execution: BackgroundTaskExecutor, InProcessInvoker - High-level: Magic facade, RemotePointer, async wrappers - Discovery/external code loading/Window groundwork (PEB/TEB, pattern scanning, raw allocations, DLL external code loading, window/input) Tests: 180 passing, 4 integration/interactive tests skipped.
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace WhiteMagic.Discovery;
|
||||
|
||||
/// <summary>
|
||||
/// Scans process memory for a byte pattern with an optional wildcard mask.
|
||||
/// </summary>
|
||||
public static class PatternScanner
|
||||
{
|
||||
/// <summary>
|
||||
/// Scans a memory range for the first occurrence of a pattern with an optional wildcard mask.
|
||||
/// </summary>
|
||||
/// <param name="memory">The memory accessor.</param>
|
||||
/// <param name="pattern">The byte pattern to search for.</param>
|
||||
/// <param name="mask">
|
||||
/// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard".
|
||||
/// If <see langword="null"/>, all bytes are treated as 'x' (exact match).
|
||||
/// </param>
|
||||
/// <param name="start">The starting address of the scan range.</param>
|
||||
/// <param name="end">The ending address (exclusive) of the scan range.</param>
|
||||
/// <returns>The address of the first match, or <see cref="IntPtr.Zero"/> if not found.</returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="pattern"/> is empty, or <paramref name="mask"/> length does not match
|
||||
/// <paramref name="pattern"/> length, or <paramref name="mask"/> contains invalid characters.
|
||||
/// </exception>
|
||||
/// <exception cref="Win32Exception">Memory read fails with an unexpected error.</exception>
|
||||
public static IntPtr Find(
|
||||
MemoryBase memory,
|
||||
byte[] pattern,
|
||||
string? mask,
|
||||
IntPtr start,
|
||||
IntPtr end)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(memory);
|
||||
ArgumentNullException.ThrowIfNull(pattern);
|
||||
|
||||
if (pattern.Length == 0)
|
||||
throw new ArgumentException("Pattern cannot be empty.", nameof(pattern));
|
||||
|
||||
// Validate and normalize mask
|
||||
if (mask is not null)
|
||||
{
|
||||
if (mask.Length != pattern.Length)
|
||||
throw new ArgumentException(
|
||||
$"Mask length ({mask.Length}) must match pattern length ({pattern.Length}).",
|
||||
nameof(mask));
|
||||
|
||||
foreach (char c in mask)
|
||||
{
|
||||
if (c != 'x' && c != '?')
|
||||
throw new ArgumentException(
|
||||
$"Mask may contain only 'x' (match) or '?' (wildcard); found '{c}'.",
|
||||
nameof(mask));
|
||||
}
|
||||
}
|
||||
|
||||
// Null mask means treat all bytes as 'x' (exact match)
|
||||
mask ??= new string('x', pattern.Length);
|
||||
|
||||
// Scan range in reasonable chunks (64 KB to avoid massive single reads)
|
||||
const int chunkSize = 64 * 1024;
|
||||
int patternLen = pattern.Length;
|
||||
long rangeSize = (long)end - (long)start;
|
||||
|
||||
if (rangeSize <= 0)
|
||||
return IntPtr.Zero;
|
||||
|
||||
// For small ranges, read all at once
|
||||
if (rangeSize <= chunkSize)
|
||||
{
|
||||
byte[] buffer = memory.ReadBytes(start, (int)rangeSize);
|
||||
return FindInBuffer(buffer, pattern, mask, start);
|
||||
}
|
||||
|
||||
// For larger ranges, scan in chunks
|
||||
long remaining = rangeSize;
|
||||
IntPtr current = start;
|
||||
|
||||
while (remaining > 0)
|
||||
{
|
||||
int toRead = (int)Math.Min(chunkSize, remaining);
|
||||
byte[] chunk = memory.ReadBytes(current, toRead);
|
||||
|
||||
// Empty read means we hit an unmapped region or read failure
|
||||
if (chunk.Length == 0)
|
||||
{
|
||||
// Skip past this unreadable region
|
||||
current += toRead;
|
||||
remaining -= toRead;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Search in this chunk
|
||||
IntPtr found = FindInBuffer(chunk, pattern, mask, current);
|
||||
if (found != IntPtr.Zero)
|
||||
return found;
|
||||
|
||||
// Move to next chunk, leaving room for pattern that might straddle boundary
|
||||
// We advance by (chunkSize - patternLen + 1) to ensure we don't miss matches
|
||||
int advance = toRead - patternLen + 1;
|
||||
if (advance <= 0)
|
||||
advance = toRead;
|
||||
|
||||
current += advance;
|
||||
remaining -= advance;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans a module's memory region (from its base address through its size) for a pattern.
|
||||
/// </summary>
|
||||
/// <param name="memory">The memory accessor.</param>
|
||||
/// <param name="pattern">The byte pattern to search for.</param>
|
||||
/// <param name="mask">
|
||||
/// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard".
|
||||
/// If <see langword="null"/>, all bytes are treated as 'x' (exact match).
|
||||
/// </param>
|
||||
/// <param name="module">The module to scan.</param>
|
||||
/// <returns>The address of the first match, or <see cref="IntPtr.Zero"/> if not found.</returns>
|
||||
public static IntPtr FindInModule(
|
||||
MemoryBase memory,
|
||||
byte[] pattern,
|
||||
string? mask,
|
||||
ProcessModule module)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(module);
|
||||
|
||||
IntPtr start = module.BaseAddress;
|
||||
IntPtr end = start + module.ModuleMemorySize;
|
||||
|
||||
return Find(memory, pattern, mask, start, end);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans multiple modules for a pattern, returning the first match found.
|
||||
/// </summary>
|
||||
/// <param name="memory">The memory accessor.</param>
|
||||
/// <param name="pattern">The byte pattern to search for.</param>
|
||||
/// <param name="mask">
|
||||
/// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard".
|
||||
/// If <see langword="null"/>, all bytes are treated as 'x' (exact match).
|
||||
/// </param>
|
||||
/// <param name="modules">The modules to scan, in order.</param>
|
||||
/// <returns>The address of the first match, or <see cref="IntPtr.Zero"/> if not found.</returns>
|
||||
public static IntPtr FindInModules(
|
||||
MemoryBase memory,
|
||||
byte[] pattern,
|
||||
string? mask,
|
||||
IEnumerable<ProcessModule> modules)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(modules);
|
||||
|
||||
foreach (var module in modules)
|
||||
{
|
||||
IntPtr found = FindInModule(memory, pattern, mask, module);
|
||||
if (found != IntPtr.Zero)
|
||||
return found;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches a buffer for the first pattern match given a mask.
|
||||
/// </summary>
|
||||
private static IntPtr FindInBuffer(
|
||||
byte[] buffer,
|
||||
byte[] pattern,
|
||||
string mask,
|
||||
IntPtr bufferBase)
|
||||
{
|
||||
if (buffer.Length < pattern.Length)
|
||||
return IntPtr.Zero;
|
||||
|
||||
int patternLen = pattern.Length;
|
||||
int maxOffset = buffer.Length - patternLen;
|
||||
|
||||
for (int offset = 0; offset <= maxOffset; offset++)
|
||||
{
|
||||
bool match = true;
|
||||
|
||||
for (int i = 0; i < patternLen; i++)
|
||||
{
|
||||
// Only compare if mask says 'x' (exact match required)
|
||||
if (mask[i] == 'x' && buffer[offset + i] != pattern[i])
|
||||
{
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match)
|
||||
return bufferBase + offset;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace WhiteMagic.Discovery;
|
||||
|
||||
/// <summary>
|
||||
/// Caches pattern scan results to avoid repeated scans of the same memory range.
|
||||
/// </summary>
|
||||
public sealed class PatternScannerCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<CacheKey, IntPtr> _cache = new();
|
||||
private readonly MemoryBase _memory;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new cache for the given memory accessor.
|
||||
/// </summary>
|
||||
/// <param name="memory">The memory accessor to scan.</param>
|
||||
public PatternScannerCache(MemoryBase memory)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(memory);
|
||||
_memory = memory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a pattern, returning a cached result if available.
|
||||
/// </summary>
|
||||
/// <param name="pattern">The byte pattern to search for.</param>
|
||||
/// <param name="mask">
|
||||
/// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard".
|
||||
/// If <see langword="null"/>, all bytes are treated as 'x' (exact match).
|
||||
/// </param>
|
||||
/// <param name="start">The starting address of the scan range.</param>
|
||||
/// <param name="end">The ending address (exclusive) of the scan range.</param>
|
||||
/// <returns>
|
||||
/// The address of the first match from cache or memory, or <see cref="IntPtr.Zero"/> if not found.
|
||||
/// </returns>
|
||||
public IntPtr FindCached(
|
||||
byte[] pattern,
|
||||
string? mask,
|
||||
IntPtr start,
|
||||
IntPtr end)
|
||||
{
|
||||
var key = new CacheKey(pattern, mask, start, end);
|
||||
|
||||
// Try to get from cache first
|
||||
if (_cache.TryGetValue(key, out IntPtr cached))
|
||||
return cached;
|
||||
|
||||
// Not in cache, perform the scan
|
||||
IntPtr found = PatternScanner.Find(_memory, pattern, mask, start, end);
|
||||
|
||||
// Cache the result (even if Zero)
|
||||
_cache[key] = found;
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a pattern within a module, returning a cached result if available.
|
||||
/// </summary>
|
||||
/// <param name="pattern">The byte pattern to search for.</param>
|
||||
/// <param name="mask">
|
||||
/// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard".
|
||||
/// If <see langword="null"/>, all bytes are treated as 'x' (exact match).
|
||||
/// </param>
|
||||
/// <param name="module">The module to scan.</param>
|
||||
/// <returns>
|
||||
/// The address of the first match from cache or memory, or <see cref="IntPtr.Zero"/> if not found.
|
||||
/// </returns>
|
||||
public IntPtr FindInModuleCached(
|
||||
byte[] pattern,
|
||||
string? mask,
|
||||
ProcessModule module)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(module);
|
||||
|
||||
IntPtr start = module.BaseAddress;
|
||||
IntPtr end = start + module.ModuleMemorySize;
|
||||
|
||||
return FindCached(pattern, mask, start, end);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a pattern across multiple modules, returning a cached result if available.
|
||||
/// </summary>
|
||||
/// <param name="pattern">The byte pattern to search for.</param>
|
||||
/// <param name="mask">
|
||||
/// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard".
|
||||
/// If <see langword="null"/>, all bytes are treated as 'x' (exact match).
|
||||
/// </param>
|
||||
/// <param name="modules">The modules to scan, in order.</param>
|
||||
/// <returns>
|
||||
/// The address of the first match from cache or memory, or <see cref="IntPtr.Zero"/> if not found.
|
||||
/// </returns>
|
||||
public IntPtr FindInModulesCached(
|
||||
byte[] pattern,
|
||||
string? mask,
|
||||
IEnumerable<ProcessModule> modules)
|
||||
{
|
||||
// For multiple modules, we use a combined key (all modules hashed together)
|
||||
// This is less granular but still useful for repeated queries
|
||||
var moduleList = modules.ToList();
|
||||
var key = new CacheKey(pattern, mask, IntPtr.Zero, IntPtr.Zero, Modules: moduleList);
|
||||
if (_cache.TryGetValue(key, out IntPtr cached))
|
||||
return cached;
|
||||
|
||||
IntPtr found = PatternScanner.FindInModules(_memory, pattern, mask, moduleList);
|
||||
|
||||
_cache[key] = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all cached scan results.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_cache.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cache key combining pattern, mask, and address range.
|
||||
/// </summary>
|
||||
private sealed record CacheKey(
|
||||
byte[] Pattern,
|
||||
string? Mask,
|
||||
IntPtr Start,
|
||||
IntPtr End,
|
||||
IReadOnlyList<ProcessModule>? Modules = null) : IEquatable<CacheKey>
|
||||
{
|
||||
// Override GetHashCode to hash the contents, not references
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = new HashCode();
|
||||
|
||||
// Hash pattern bytes
|
||||
foreach (byte b in Pattern)
|
||||
hash.Add(b);
|
||||
|
||||
// Hash mask
|
||||
hash.Add(Mask?.GetHashCode() ?? 0);
|
||||
|
||||
// Hash address range
|
||||
hash.Add(Start.GetHashCode());
|
||||
hash.Add(End.GetHashCode());
|
||||
|
||||
// Hash modules if present (by base address)
|
||||
if (Modules is not null)
|
||||
{
|
||||
foreach (var m in Modules)
|
||||
hash.Add(m.BaseAddress.GetHashCode());
|
||||
}
|
||||
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagic.Discovery;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a section in a PE file.
|
||||
/// </summary>
|
||||
public readonly record struct PeSection
|
||||
{
|
||||
/// <summary>
|
||||
/// The 8-byte null-terminated section name (e.g., ".text", ".data").
|
||||
/// </summary>
|
||||
public string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The virtual address of the section when loaded into memory (RVA).
|
||||
/// </summary>
|
||||
public IntPtr VirtualAddress { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of the section in memory.
|
||||
/// </summary>
|
||||
public int VirtualSize { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses PE headers to expose section information and entry points.
|
||||
/// </summary>
|
||||
public sealed class PeHeaderParser
|
||||
{
|
||||
private readonly MemoryBase _memory;
|
||||
private readonly IntPtr _baseAddress;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new PE header parser for the module at the specified base address.
|
||||
/// </summary>
|
||||
/// <param name="memory">The memory accessor.</param>
|
||||
/// <param name="baseAddress">The base address of the module.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the entry point RVA (Relative Virtual Address) of the PE file.
|
||||
/// </summary>
|
||||
/// <returns>The entry point RVA, or <see cref="IntPtr.Zero"/> if unavailable.</returns>
|
||||
/// <exception cref="Win32Exception">Reading memory fails.</exception>
|
||||
/// <exception cref="InvalidDataException">The PE headers are invalid.</exception>
|
||||
public IntPtr EntryPoint
|
||||
{
|
||||
get
|
||||
{
|
||||
// Read and parse PE headers
|
||||
var (optionalHeader, _) = ParseOptionalHeader();
|
||||
|
||||
if (optionalHeader is null)
|
||||
return IntPtr.Zero;
|
||||
|
||||
// Entry point is at different offsets for PE32 vs PE32+
|
||||
bool isPe32Plus = IsPe32Plus();
|
||||
|
||||
if (isPe32Plus)
|
||||
{
|
||||
// PE32+: AddressOfEntryPoint is at offset 16 in OPTIONAL_HEADER (64-bit)
|
||||
return (IntPtr)BitConverter.ToUInt32(
|
||||
optionalHeader.AsSpan(16, 4));
|
||||
}
|
||||
else
|
||||
{
|
||||
// PE32: AddressOfEntryPoint is at offset 16 in OPTIONAL_HEADER (32-bit)
|
||||
return (IntPtr)BitConverter.ToUInt32(
|
||||
optionalHeader.AsSpan(16, 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates all sections in the PE file.
|
||||
/// </summary>
|
||||
/// <returns>An enumerable of PE sections.</returns>
|
||||
/// <exception cref="Win32Exception">Reading memory fails.</exception>
|
||||
/// <exception cref="InvalidDataException">The PE headers are invalid.</exception>
|
||||
public IEnumerable<PeSection> 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the DOS header, PE signature, and optional header.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses just the optional header (for entry point).
|
||||
/// </summary>
|
||||
private (byte[]? OptionalHeader, byte[][]? SectionHeaders) ParseOptionalHeader()
|
||||
{
|
||||
return ParseOptionalHeaderAndSectionHeaders();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the PE file is PE32+ (64-bit) or PE32 (32-bit).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a null-terminated 8-byte section name.
|
||||
/// </summary>
|
||||
private static string ParseSectionName(byte[] sectionHeader)
|
||||
{
|
||||
// Name is first 8 bytes
|
||||
var nameBytes = new Span<byte>(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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user