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:
kbe
2026-07-21 23:43:14 +02:00
parent a0ca7050a2
commit 3f0bea6bd4
44 changed files with 5595 additions and 84 deletions
@@ -0,0 +1,201 @@
using System.Runtime.InteropServices;
using WhiteMagic;
using WhiteMagic.Discovery;
namespace WhiteMagicTest.Discovery;
/// <summary>
/// Tests for <see cref="PatternScannerCache"/>.
/// </summary>
public class PatternScannerCacheTests
{
private static InProcessReader CreateReader()
{
return new InProcessReader();
}
[Fact]
public void FindCached_returns_same_result_on_second_call()
{
using var reader = CreateReader();
var cache = new PatternScannerCache(reader);
// Create a buffer with a known pattern
byte[] buffer = new byte[256];
buffer[30] = 0x11;
buffer[31] = 0x22;
buffer[32] = 0x33;
buffer[33] = 0x44;
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
IntPtr end = addr + buffer.Length;
byte[] pattern = { 0x11, 0x22, 0x33, 0x44 };
// First call should scan memory
IntPtr first = cache.FindCached(pattern, null, addr, end);
// Second call should return cached result
IntPtr second = cache.FindCached(pattern, null, addr, end);
Assert.Equal(addr + 30, first);
Assert.Equal(first, second);
}
finally
{
pin.Free();
}
}
[Fact]
public void FindCached_different_ranges_are_cached_separately()
{
using var reader = CreateReader();
var cache = new PatternScannerCache(reader);
// Create two separate buffers
byte[] buffer1 = new byte[128];
buffer1[10] = 0xAA;
buffer1[11] = 0xBB;
byte[] buffer2 = new byte[128];
buffer2[20] = 0xAA;
buffer2[21] = 0xBB;
GCHandle pin1 = GCHandle.Alloc(buffer1, GCHandleType.Pinned);
GCHandle pin2 = GCHandle.Alloc(buffer2, GCHandleType.Pinned);
try
{
IntPtr addr1 = pin1.AddrOfPinnedObject();
IntPtr end1 = addr1 + buffer1.Length;
IntPtr addr2 = pin2.AddrOfPinnedObject();
IntPtr end2 = addr2 + buffer2.Length;
byte[] pattern = { 0xAA, 0xBB };
IntPtr found1 = cache.FindCached(pattern, null, addr1, end1);
IntPtr found2 = cache.FindCached(pattern, null, addr2, end2);
Assert.Equal(addr1 + 10, found1);
Assert.Equal(addr2 + 20, found2);
Assert.NotEqual(found1, found2);
}
finally
{
pin1.Free();
pin2.Free();
}
}
[Fact]
public void FindCached_with_mask_caches_correctly()
{
using var reader = CreateReader();
var cache = new PatternScannerCache(reader);
byte[] buffer = new byte[256];
buffer[40] = 0x99;
buffer[41] = 0x88; // This is wildcard
buffer[42] = 0x77;
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
IntPtr end = addr + buffer.Length;
byte[] pattern = { 0x99, 0x00, 0x77 };
string mask = "x?x";
IntPtr first = cache.FindCached(pattern, mask, addr, end);
IntPtr second = cache.FindCached(pattern, mask, addr, end);
Assert.Equal(addr + 40, first);
Assert.Equal(first, second);
}
finally
{
pin.Free();
}
}
[Fact]
public void Clear_clears_cached_results()
{
using var reader = CreateReader();
var cache = new PatternScannerCache(reader);
byte[] buffer = new byte[256];
buffer[50] = 0xCC;
buffer[51] = 0xDD;
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
IntPtr end = addr + buffer.Length;
byte[] pattern = { 0xCC, 0xDD };
// Cache a result
IntPtr first = cache.FindCached(pattern, null, addr, end);
Assert.Equal(addr + 50, first);
// Clear the cache
cache.Clear();
// This should rescan (not return cached result)
IntPtr second = cache.FindCached(pattern, null, addr, end);
Assert.Equal(addr + 50, second);
}
finally
{
pin.Free();
}
}
[Fact]
public void FindInModuleCached_caches_module_scans()
{
using var reader = CreateReader();
var cache = new PatternScannerCache(reader);
var currentProcess = System.Diagnostics.Process.GetCurrentProcess();
var mainModule = currentProcess.MainModule;
Assert.NotNull(mainModule);
// MZ header is always at the start of the main module
byte[] pattern = { 0x4D, 0x5A };
IntPtr first = cache.FindInModuleCached(pattern, null, mainModule);
IntPtr second = cache.FindInModuleCached(pattern, null, mainModule);
Assert.Equal(mainModule.BaseAddress, first);
Assert.Equal(first, second);
}
[Fact]
public void FindInModulesCached_caches_multiple_modules()
{
using var reader = CreateReader();
var cache = new PatternScannerCache(reader);
var currentProcess = System.Diagnostics.Process.GetCurrentProcess();
var modules = currentProcess.Modules.Cast<System.Diagnostics.ProcessModule>().ToList();
Assert.NotEmpty(modules);
// MZ header should be present in at least one module
byte[] pattern = { 0x4D, 0x5A };
IntPtr first = cache.FindInModulesCached(pattern, null, modules);
IntPtr second = cache.FindInModulesCached(pattern, null, modules);
Assert.NotEqual(IntPtr.Zero, first);
Assert.Equal(first, second);
}
}
@@ -0,0 +1,188 @@
using System.Runtime.InteropServices;
using WhiteMagic;
using WhiteMagic.Discovery;
using WhiteMagicTest;
namespace WhiteMagicTest.Discovery;
/// <summary>
/// Tests for <see cref="PatternScanner"/>.
/// </summary>
public class PatternScannerTests
{
private static InProcessReader CreateReader()
{
return new InProcessReader();
}
[Fact]
public void Find_exact_pattern_returns_correct_address()
{
using var reader = CreateReader();
// Create a buffer with known bytes
byte[] buffer = new byte[256];
buffer[10] = 0xDE;
buffer[11] = 0xAD;
buffer[12] = 0xBE;
buffer[13] = 0xEF;
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
IntPtr end = addr + buffer.Length;
// Search for the exact pattern
byte[] pattern = { 0xDE, 0xAD, 0xBE, 0xEF };
IntPtr found = PatternScanner.Find(reader, pattern, null, addr, end);
Assert.Equal(addr + 10, found);
}
finally
{
pin.Free();
}
}
[Fact]
public void Find_with_wildcard_mask_ignores_wildcard_bytes()
{
using var reader = CreateReader();
// Create a buffer with known bytes
byte[] buffer = new byte[256];
buffer[20] = 0x12;
buffer[21] = 0x34; // This byte is wildcard
buffer[22] = 0x56;
buffer[23] = 0x78;
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
IntPtr end = addr + buffer.Length;
// Search with wildcard mask (x = match, ? = wildcard)
byte[] pattern = { 0x12, 0x00, 0x56, 0x78 };
string mask = "x?xx"; // Second byte is wildcard
IntPtr found = PatternScanner.Find(reader, pattern, mask, addr, end);
Assert.Equal(addr + 20, found);
}
finally
{
pin.Free();
}
}
[Fact]
public void Find_pattern_not_found_returns_zero()
{
using var reader = CreateReader();
// Create a buffer without the target pattern
byte[] buffer = new byte[256];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = 0xAA;
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
IntPtr end = addr + buffer.Length;
// Search for pattern that doesn't exist
byte[] pattern = { 0xDE, 0xAD, 0xBE, 0xEF };
IntPtr found = PatternScanner.Find(reader, pattern, null, addr, end);
Assert.Equal(IntPtr.Zero, found);
}
finally
{
pin.Free();
}
}
[Fact]
public void Find_empty_pattern_throws()
{
using var reader = CreateReader();
byte[] pattern = Array.Empty<byte>();
var ex = Assert.Throws<ArgumentException>(() =>
PatternScanner.Find(reader, pattern, null, IntPtr.Zero, (IntPtr)1000));
Assert.Contains("Pattern cannot be empty", ex.Message);
}
[Fact]
public void Find_mask_length_mismatch_throws()
{
using var reader = CreateReader();
byte[] pattern = { 0xDE, 0xAD, 0xBE, 0xEF };
string mask = "xxx"; // Wrong length
var ex = Assert.Throws<ArgumentException>(() =>
PatternScanner.Find(reader, pattern, mask, IntPtr.Zero, (IntPtr)1000));
Assert.Contains("Mask length", ex.Message);
}
[Fact]
public void Find_invalid_mask_char_throws()
{
using var reader = CreateReader();
byte[] pattern = { 0xDE, 0xAD, 0xBE, 0xEF };
string mask = "axxx"; // 'a' is invalid
var ex = Assert.Throws<ArgumentException>(() =>
PatternScanner.Find(reader, pattern, mask, IntPtr.Zero, (IntPtr)1000));
}
[Fact]
public void Find_null_mask_treats_all_as_exact()
{
using var reader = CreateReader();
byte[] buffer = new byte[256];
buffer[50] = 0xAB;
buffer[51] = 0xCD;
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
IntPtr addr = pin.AddrOfPinnedObject();
IntPtr end = addr + buffer.Length;
// Null mask should behave like "xx" (exact match)
byte[] pattern = { 0xAB, 0xCD };
IntPtr found = PatternScanner.Find(reader, pattern, null, addr, end);
Assert.Equal(addr + 50, found);
}
finally
{
pin.Free();
}
}
[Fact]
public void FindInModule_scans_current_process_module()
{
using var reader = CreateReader();
// Get the current process's main module
var currentProcess = System.Diagnostics.Process.GetCurrentProcess();
var mainModule = currentProcess.MainModule;
Assert.NotNull(mainModule);
// MZ header is always at the start of the main module
byte[] pattern = { 0x4D, 0x5A };
IntPtr found = PatternScanner.FindInModule(reader, pattern, null, mainModule);
Assert.Equal(mainModule.BaseAddress, found);
}
}
@@ -0,0 +1,156 @@
using WhiteMagic;
using WhiteMagic.Discovery;
namespace WhiteMagicTest.Discovery;
/// <summary>
/// Tests for <see cref="PeHeaderParser"/>.
/// </summary>
public class PeHeaderParserTests
{
private static InProcessReader CreateReader()
{
return new InProcessReader();
}
[Fact]
public void EntryPoint_returns_nonzero_for_current_module()
{
using var reader = CreateReader();
var currentProcess = System.Diagnostics.Process.GetCurrentProcess();
var mainModule = currentProcess.MainModule;
Assert.NotNull(mainModule);
var parser = new PeHeaderParser(reader, mainModule.BaseAddress);
IntPtr entryPoint = parser.EntryPoint;
// Entry point should be a valid RVA (non-zero for a valid PE)
Assert.NotEqual(IntPtr.Zero, entryPoint);
// Entry point should be less than module size
Assert.True((nint)entryPoint < mainModule.ModuleMemorySize);
}
[Fact]
public void Sections_enumerates_at_least_text_section()
{
using var reader = CreateReader();
var currentProcess = System.Diagnostics.Process.GetCurrentProcess();
var mainModule = currentProcess.MainModule;
Assert.NotNull(mainModule);
var parser = new PeHeaderParser(reader, mainModule.BaseAddress);
var sections = parser.Sections.ToList();
Assert.NotEmpty(sections);
// Every PE file should have a .text section (or similar)
var textSection = sections.FirstOrDefault(s =>
s.Name.Equals(".text", StringComparison.OrdinalIgnoreCase) ||
s.Name.Equals("TEXT", StringComparison.OrdinalIgnoreCase));
// May not find ".text" exactly, but should have at least some sections
Assert.True(sections.Count >= 1);
}
[Fact]
public void Sections_have_valid_properties()
{
using var reader = CreateReader();
var currentProcess = System.Diagnostics.Process.GetCurrentProcess();
var mainModule = currentProcess.MainModule;
Assert.NotNull(mainModule);
var parser = new PeHeaderParser(reader, mainModule.BaseAddress);
var sections = parser.Sections.ToList();
foreach (var section in sections)
{
// Name should not be empty
Assert.False(string.IsNullOrWhiteSpace(section.Name));
// Virtual address should be within module bounds
Assert.True((nint)section.VirtualAddress < mainModule.ModuleMemorySize);
// Virtual size should be positive
Assert.True(section.VirtualSize > 0);
}
}
[Fact]
public void Sections_have_common_names()
{
using var reader = CreateReader();
var currentProcess = System.Diagnostics.Process.GetCurrentProcess();
var mainModule = currentProcess.MainModule;
Assert.NotNull(mainModule);
var parser = new PeHeaderParser(reader, mainModule.BaseAddress);
var sections = parser.Sections.Select(s => s.Name).ToList();
// At least some common section names should be present
var commonNames = new[] { ".text", ".data", ".rdata", ".bss" };
bool hasCommonSection = commonNames.Any(name =>
sections.Contains(name, StringComparer.OrdinalIgnoreCase));
// This might not always be true, but for managed EXEs it usually is
// We'll just verify sections were enumerated
Assert.NotEmpty(sections);
}
[Fact]
public void EntryPoint_is_consistent_across_calls()
{
using var reader = CreateReader();
var currentProcess = System.Diagnostics.Process.GetCurrentProcess();
var mainModule = currentProcess.MainModule;
Assert.NotNull(mainModule);
var parser = new PeHeaderParser(reader, mainModule.BaseAddress);
IntPtr first = parser.EntryPoint;
IntPtr second = parser.EntryPoint;
Assert.Equal(first, second);
}
[Fact]
public void Sections_are_consistent_across_calls()
{
using var reader = CreateReader();
var currentProcess = System.Diagnostics.Process.GetCurrentProcess();
var mainModule = currentProcess.MainModule;
Assert.NotNull(mainModule);
var parser = new PeHeaderParser(reader, mainModule.BaseAddress);
var first = parser.Sections.ToList();
var second = parser.Sections.ToList();
Assert.Equal(first.Count, second.Count);
for (int i = 0; i < first.Count; i++)
{
Assert.Equal(first[i].Name, second[i].Name);
Assert.Equal(first[i].VirtualAddress, second[i].VirtualAddress);
Assert.Equal(first[i].VirtualSize, second[i].VirtualSize);
}
}
[Fact]
public void Constructor_throws_on_zero_base_address()
{
using var reader = CreateReader();
var ex = Assert.Throws<ArgumentException>(() =>
new PeHeaderParser(reader, IntPtr.Zero));
Assert.Contains("Base address cannot be zero", ex.Message);
}
}