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.
202 lines
6.9 KiB
C#
202 lines
6.9 KiB
C#
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;
|
|
}
|
|
}
|