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.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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user