using System.ComponentModel; using System.Diagnostics; namespace WhiteMagic.Discovery; /// /// Scans process memory for a byte pattern with an optional wildcard mask. /// public static class PatternScanner { /// /// Scans a memory range for the first occurrence of a pattern with an optional wildcard mask. /// /// The memory accessor. /// The byte pattern to search for. /// /// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard". /// If , all bytes are treated as 'x' (exact match). /// /// The starting address of the scan range. /// The ending address (exclusive) of the scan range. /// The address of the first match, or if not found. /// /// is empty, or length does not match /// length, or contains invalid characters. /// /// Memory read fails with an unexpected error. 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; } /// /// Scans a module's memory region (from its base address through its size) for a pattern. /// /// The memory accessor. /// The byte pattern to search for. /// /// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard". /// If , all bytes are treated as 'x' (exact match). /// /// The module to scan. /// The address of the first match, or if not found. 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); } /// /// Scans multiple modules for a pattern, returning the first match found. /// /// The memory accessor. /// The byte pattern to search for. /// /// A mask string where 'x' means "match this byte exactly" and '?' means "wildcard". /// If , all bytes are treated as 'x' (exact match). /// /// The modules to scan, in order. /// The address of the first match, or if not found. public static IntPtr FindInModules( MemoryBase memory, byte[] pattern, string? mask, IEnumerable 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; } /// /// Searches a buffer for the first pattern match given a mask. /// 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; } }