diff --git a/.gitignore b/.gitignore index add57be..0976324 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ bin/ obj/ /packages/ riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +/_ReSharper.Caches/ +/.vs diff --git a/MemNet.sln b/MemNet.sln index af95f23..47a4a40 100644 --- a/MemNet.sln +++ b/MemNet.sln @@ -1,10 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Memlib", "MemNet\MemNet.csproj", "{CA5757C4-1576-4CE1-AD33-18C0A6E3BEEF}" +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35707.178 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MemNet", "MemNet\MemNet.csproj", "{CA5757C4-1576-4CE1-AD33-18C0A6E3BEEF}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{CB0F42DE-E293-4018-8BC0-0D82721A3717}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reader", "Examples\Pattern Scanner\Pattern Scanner.csproj", "{36FA29A7-59CB-40EA-B2D2-066D7B9F08C1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pattern Scanner", "Examples\Pattern Scanner\Pattern Scanner.csproj", "{36FA29A7-59CB-40EA-B2D2-066D7B9F08C1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testing", "Testing\Testing.csproj", "{24F0E9FF-1BDE-41E8-BE50-BE33D30AC3C6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -20,6 +25,13 @@ Global {36FA29A7-59CB-40EA-B2D2-066D7B9F08C1}.Debug|Any CPU.Build.0 = Debug|Any CPU {36FA29A7-59CB-40EA-B2D2-066D7B9F08C1}.Release|Any CPU.ActiveCfg = Release|Any CPU {36FA29A7-59CB-40EA-B2D2-066D7B9F08C1}.Release|Any CPU.Build.0 = Release|Any CPU + {24F0E9FF-1BDE-41E8-BE50-BE33D30AC3C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {24F0E9FF-1BDE-41E8-BE50-BE33D30AC3C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {24F0E9FF-1BDE-41E8-BE50-BE33D30AC3C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {24F0E9FF-1BDE-41E8-BE50-BE33D30AC3C6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {36FA29A7-59CB-40EA-B2D2-066D7B9F08C1} = {CB0F42DE-E293-4018-8BC0-0D82721A3717} diff --git a/MemNet/Wildcard.cs b/MemNet/Wildcard.cs index 1d52d32..4586147 100644 --- a/MemNet/Wildcard.cs +++ b/MemNet/Wildcard.cs @@ -11,6 +11,14 @@ public Wildcard(string token) _lowNibble = token[1] == '?' ? null : Convert.ToInt32(token[1].ToString(), 16); } + public byte? AsByte() + { + if (!_highNibble.HasValue || !_lowNibble.HasValue) + return null; + + return (byte)((_highNibble.Value << 4) | _lowNibble.Value); + } + public bool Matches(byte b) { int high = (b >> 4) & 0xF; diff --git a/Testing/AobTests.cs b/Testing/AobTests.cs new file mode 100644 index 0000000..d888dfc --- /dev/null +++ b/Testing/AobTests.cs @@ -0,0 +1,112 @@ +using Memlib; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Testing; + +[TestFixture] +internal class AobTests +{ + [Test] + [Repeat(5_000)] + public unsafe void AobGeneralTest() + { + const int length = 0x2000; + var pArr = Marshal.AllocHGlobal(length); + Span bytes; + unsafe + { + bytes = new Span(pArr.ToPointer(), length); + } + + var rand = new Random(); + rand.NextBytes(bytes); + + try + { + var pid = Environment.ProcessId; + using var mem = new Memlib.Memory(pid); + mem.Open(); + + var start = rand.Next(0, bytes.Length - 1); + var end = rand.Next(start + 1, bytes.Length); + Trace.Assert(end - start >= 0); // Sanity check + + var pattern = ScanHelper.GeneratePattern( + bytes[start..end], + out var pb0, + out var pbn, + rand); + + var results = mem.Search(pattern, pb0, pbn); + foreach (var address in results) + { + var offset = (int)(address - pArr); + + for (var j = 0; j < pattern.Length; j++) + { + var patternByte = pattern[j].AsByte(); + var matchedByte = bytes[j + offset]; + var matches = pattern[j].Matches(matchedByte); + Assert.That(matches, Is.True); + } + } + } + catch (Exception) + { + throw; + } + finally + { + Marshal.FreeHGlobal(pArr); + } + } + + [Test] + public void AobFullLengthTest() + { + byte[] bytes = [0xAA, 0xBB, 0xCC, 0xDD]; + const string pattern = "AA BB ?? DD"; + + var pid = Environment.ProcessId; + using var mem = new Memlib.Memory(pid); + mem.Open(); + + unsafe + { + fixed (byte* pb0 = bytes) + { + var results = mem.Search(pattern, new nint(pb0), new nint(pb0 + bytes.Length)); + Assert.That(results, Is.Not.Null); + Assert.That(results.Count, Is.EqualTo(1)); + var pb0nint = new nint(pb0); // Can't capture fixed locals so do a little trickery + Assert.That(results.All(p => p == pb0nint), Is.True); + } + } + } + + [Test] + public unsafe void EdgeCases() + { + var rand = new Random(); + using var mem = new Memlib.Memory(Environment.ProcessId); + mem.Open(); + + var bytes = new byte[1]; + var pattern = new Wildcard[] { new("??"), new("??") }; + fixed (byte* pb0 = bytes) + { + var pb0nint = new nint(pb0); + Assert.Throws( + () => mem.Search(pattern, pb0nint, pb0nint)); + Assert.Throws( + () => mem.Search(pattern, pb0nint, pb0nint + 1)); + } + } +} \ No newline at end of file diff --git a/Testing/ReadWriteTests.cs b/Testing/ReadWriteTests.cs new file mode 100644 index 0000000..f3981d1 --- /dev/null +++ b/Testing/ReadWriteTests.cs @@ -0,0 +1,96 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Testing; + +[TestFixture] +internal class ReadWriteTests +{ + const int Length = 0x40; + + private static unsafe void ReadTest(T src, ref T dst) where T : unmanaged + { + var defensiveSrcCopy = src; + var pid = Environment.ProcessId; + using var mem = new Memlib.Memory(pid); + mem.Open(); + + var pSrc = new nint(&src); + dst = mem.Read(pSrc); + + Assert.That(src, Is.EqualTo(defensiveSrcCopy)); + Assert.That(dst, Is.EqualTo(src)); + } + + private static unsafe void WriteTest(T src, ref T dst) where T : unmanaged + { + var defensiveSrcCopy = src; + var pid = Environment.ProcessId; + using var mem = new Memlib.Memory(pid); + mem.Open(); + + fixed (T* pDst = &dst) + { + mem.Write(new nint(pDst), src); + } + + Assert.That(src, Is.EqualTo(defensiveSrcCopy)); + Assert.That(dst, Is.EqualTo(src)); + } + + private static unsafe void PerformReadWriteTest(int length, Random rand) where T : unmanaged + { + Span src = stackalloc T[length]; + Span dst = stackalloc T[length]; + + rand.NextBytes(MemoryMarshal.AsBytes(src)); + + var pid = Environment.ProcessId; + using var mem = new Memlib.Memory(pid); + mem.Open(); + + for (var i = 0; i < length; i++) + { + ReadTest(src[i], ref dst[i]); + } + + dst.Clear(); + rand.NextBytes(MemoryMarshal.AsBytes(src)); + + for (var i = 0; i < length; i++) + { + WriteTest(src[i], ref dst[i]); + } + } + + [Test] + public unsafe void BasicReadWriteTest() + { + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + PerformReadWriteTest(Length, Random.Shared); + } +} diff --git a/Testing/ScanHelper.cs b/Testing/ScanHelper.cs new file mode 100644 index 0000000..925bdd0 --- /dev/null +++ b/Testing/ScanHelper.cs @@ -0,0 +1,54 @@ +using Memlib; +using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Testing; +internal static class ScanHelper +{ + public static byte[] GenerateBytes(int length, Random rand) + { + var bytes = new byte[length]; + rand.NextBytes(bytes); + return bytes; + } + + public static unsafe Wildcard[] GeneratePattern(ReadOnlySpan bytes, out nint start, out nint end, Random? rand = null) + { + rand ??= new Random(); + + fixed (byte* pb0 = bytes) + { + var pbn = pb0 + bytes.Length; + start = new nint(pb0); + end = new nint(pbn); + + // Sanity check + fixed (byte* endInclusive = &bytes[^1]) + Trace.Assert(pbn == endInclusive + 1); + + var pattern = new Wildcard[bytes.Length]; + var unknownChance = rand.NextSingle(); + for (var offset = 0; pb0 + offset < pbn; offset++) + { + var isUnknown = rand.NextSingle() < unknownChance; + if (isUnknown) + { + pattern[offset] = new Wildcard(); + continue; + } + + var b = pb0[offset]; + var hex = b.ToString("X2"); + pattern[offset] = new Wildcard(hex); + } + + return pattern; + } + } +} diff --git a/Testing/Testing.csproj b/Testing/Testing.csproj new file mode 100644 index 0000000..4740ea4 --- /dev/null +++ b/Testing/Testing.csproj @@ -0,0 +1,21 @@ + + + + Library + net8.0 + enable + enable + true + + + + + + + + + + + + +