From 8627e58f8e1c83319c3bf87255271a1f9b8fa693 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Thu, 11 Apr 2024 21:06:20 +0200 Subject: [PATCH 01/66] Added AllocatedRange and stubs for actual allocations --- src/MindControl/AllocatedRange.cs | 257 ++++++++++++++++++ .../InsufficientAllocatedMemoryException.cs | 30 ++ src/MindControl/MemoryRange.cs | 103 ++++++- .../ProcessMemory/ProcessMemory.Allocation.cs | 39 +++ .../ProcessMemory/ProcessMemory.FindBytes.cs | 1 - 5 files changed, 422 insertions(+), 8 deletions(-) create mode 100644 src/MindControl/AllocatedRange.cs create mode 100644 src/MindControl/InsufficientAllocatedMemoryException.cs create mode 100644 src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs diff --git a/src/MindControl/AllocatedRange.cs b/src/MindControl/AllocatedRange.cs new file mode 100644 index 0000000..246cc78 --- /dev/null +++ b/src/MindControl/AllocatedRange.cs @@ -0,0 +1,257 @@ +namespace MindControl; + +/// +/// Represents a range of allocated memory in a process. +/// +public class AllocatedRange : IDisposable +{ + /// + /// Gets the memory range of this allocated range. + /// + public MemoryRange Range { get; } + + private readonly List _reservedRanges = new(); + + /// + /// Gets the reserved ranges within this range. + /// They are owned by this instance. Do not dispose them manually. + /// + public IReadOnlyList ReservedRanges => _reservedRanges.AsReadOnly(); + + /// + /// Gets a value indicating if the memory range has executable permissions. + /// + public bool IsExecutable { get; } + + /// + /// Gets the range containing this range, if any. + /// + public AllocatedRange? ParentRange { get; } + + /// + /// Gets a boolean indicating if the range has been freed and is thus unavailable for reservations. + /// + public bool IsReserved => !_hasBeenFreed; + + private readonly ProcessMemory? _parentProcessMemory; + + private bool _hasBeenFreed; + + /// + /// Builds a new instance with a parent range. + /// + /// Memory range of the allocated range. + /// Value indicating if the memory range has executable permissions. + /// Parent range containing this range. + internal AllocatedRange(MemoryRange range, bool isExecutable, AllocatedRange parentRange) + { + Range = range; + IsExecutable = isExecutable; + ParentRange = parentRange; + } + + /// + /// Builds a new top-level instance with a parent process memory. + /// + /// Memory range of the allocated range. + /// Value indicating if the memory range has executable permissions. + /// Parent process memory instance handling this range. + internal AllocatedRange(MemoryRange range, bool isExecutable, ProcessMemory parentProcessMemory) + { + Range = range; + IsExecutable = isExecutable; + _parentProcessMemory = parentProcessMemory; + } + + /// + /// Gets the total space reserved in the range, in bytes. + /// + /// The total space reserved in the range. + public ulong GetTotalReservedSpace() + { + if (_hasBeenFreed) + throw new ObjectDisposedException(nameof(AllocatedRange)); + + return ReservedRanges.Aggregate(0, + (current, subRange) => current + subRange?.Range.GetSize() ?? 0); + } + + /// + /// Gets the total space remaining in the range, in bytes. + /// Note that this does not mean the range is contiguous. Space might be fragmented and thus unable to be + /// reserved entirely in a single allocation. + /// + /// The total space remaining in the range. + public ulong GetRemainingSpace() + { + if (_hasBeenFreed) + throw new ObjectDisposedException(nameof(AllocatedRange)); + + return Range.GetSize() - GetTotalReservedSpace(); + } + + /// + /// Gets the largest contiguous, unreserved space in the range. This is the largest space that can be reserved in a + /// single allocation. + /// + /// The largest contiguous, unreserved space in the range. + public MemoryRange? GetLargestReservableSpace() + { + if (_hasBeenFreed) + throw new ObjectDisposedException(nameof(AllocatedRange)); + + return GetFreeRanges().OrderByDescending(r => r.GetSize()).FirstOrDefault(); + } + + /// + /// Gets a collection of all contiguous memory ranges that can be used for reservations. + /// + private IEnumerable GetFreeRanges() + { + // Take the whole range and exclude all reserved ranges from it. + // This will split the range into all free ranges. + var freeRanges = new List { Range }; + foreach (var reservedRange in _reservedRanges) + { + freeRanges = freeRanges.SelectMany(r => r.Exclude(reservedRange.Range)).ToList(); + } + + return freeRanges; + } + + /// + /// Gets the first free range that can fit the specified size, with an optional alignment, or null if no range is + /// large enough to fit the requested size with the specified alignment. + /// Note that this method does not reserve the range. Most of the time, you should use + /// instead of this method. + /// + /// Requested size of the range to find, in bytes. + /// Optional byte alignment for the range. When null, values are not aligned. The + /// default value is 8, meaning that for example a range of [0x15,0x3C] will be aligned to [0x18,0x38] and thus + /// only accomodate 32 bytes. + /// The first free range that can fit the specified size, or null if no range is large enough. + public MemoryRange? GetNextRangeFittingSize(ulong size, uint? byteAlignment = 8) + { + if (_hasBeenFreed) + throw new ObjectDisposedException(nameof(AllocatedRange)); + + var matchingRange = GetFreeRanges() + .Select(r => byteAlignment == null ? r : r.AlignedTo(byteAlignment.Value)) + .OrderBy(r => r.Start.ToUInt64()) + .Cast() + .FirstOrDefault(r => r?.GetSize() > size); + + if (matchingRange == null) + return null; + + // Adjust the end address to fit no more than the requested size + return matchingRange.Value with { End = (UIntPtr)(matchingRange.Value.Start.ToUInt64() + size - 1) }; + } + + /// + /// Reserves and returns the next available range with the specified size. + /// Data stored in the returned range will not be overwritten by future reservations until the range is freed. + /// This method throws if there is no contiguous memory large enough in the range. See + /// for a non-throwing alternative. + /// + /// Minimal size that the range to get must be able to accomodate, in bytes. + /// Optional byte alignment for the range. When null, values are not aligned. The + /// default value is 8, meaning that for example a range of [0x15,0x3C] will be aligned to [0x18,0x38] and thus + /// only accomodate 32 bytes. + /// The reserved range. + public AllocatedRange ReserveRange(ulong size, uint? byteAlignment = 8) + => TryReserveRange(size, byteAlignment) ?? throw new InsufficientAllocatedMemoryException(size, byteAlignment); + + /// + /// Attempts to reserve and return the next available range with the specified size. + /// Data stored in the returned range will not be overwritten by future reservations until the range is freed. + /// This method returns null if there is no contiguous memory large enough in the range. See + /// if you expect an exception to be thrown on a reservation failure. + /// Note that this method will still throw if the range has been freed. + /// + /// Minimal size that the range to get must be able to accomodate, in bytes. + /// Optional byte alignment for the range. When null, values are not aligned. The + /// default value is 8, meaning that for example a range of [0x15,0x3C] will be aligned to [0x18,0x38] and thus + /// only accomodate 32 bytes. + /// The reserved range. + public AllocatedRange? TryReserveRange(ulong size, uint? byteAlignment = 8) + { + if (_hasBeenFreed) + throw new ObjectDisposedException(nameof(AllocatedRange)); + + var range = GetNextRangeFittingSize(size, byteAlignment); + if (range == null) + return null; + + var reservedRange = new AllocatedRange(range.Value, IsExecutable, this); + _reservedRanges.Add(reservedRange); + return reservedRange; + } + + /// + /// Makes reserved space matching the specified range available for future reservations. Affected reserved ranges + /// will be disposed, and may be either completely gone, reduced, or split into two ranges. + /// Consider disposing reservations instead, unless you want more control over the range to free. + /// + /// The range of memory to free from reservations in this instance. + public void FreeRange(MemoryRange rangeToFree) + { + for (int i = _reservedRanges.Count - 1; i >= 0; i--) + { + var currentRange = _reservedRanges[i]; + var resultingRanges = currentRange.Range.Exclude(rangeToFree).ToArray(); + + // Reservation is completely gone + if (resultingRanges.Length == 0) + currentRange.Dispose(); + + // Reservation is reduced + else if (resultingRanges.Length == 1 && resultingRanges[0] != currentRange.Range) + { + currentRange.Dispose(); + _reservedRanges[i] = new AllocatedRange(resultingRanges[0], IsExecutable, this); + } + + // Reservation is split into two ranges + else if (resultingRanges.Length == 2) + { + currentRange.Dispose(); + _reservedRanges[i] = new AllocatedRange(resultingRanges[0], IsExecutable, this); + _reservedRanges.Insert(i + 1, new AllocatedRange(resultingRanges[1], IsExecutable, this)); + } + } + } + + /// + /// Removes all reserved ranges from the range, meaning the whole range will be available for reservations. + /// After using this method, data stored in this range may be overwritten by future reservations. + /// + public void Clear() + { + if (_hasBeenFreed) + return; + + foreach (var subRange in _reservedRanges) + subRange.Dispose(); + + _reservedRanges.Clear(); + } + + /// + /// Releases all reservations used by the , and makes this instance unusable. + /// If this instance is a top-level range, the process memory will be freed as well. + /// + public void Dispose() + { + if (_hasBeenFreed) + return; + + _hasBeenFreed = true; + Clear(); + + if (ParentRange != null) + ParentRange._reservedRanges.Remove(this); + else + _parentProcessMemory?.Free(this); + } +} \ No newline at end of file diff --git a/src/MindControl/InsufficientAllocatedMemoryException.cs b/src/MindControl/InsufficientAllocatedMemoryException.cs new file mode 100644 index 0000000..9d0aa93 --- /dev/null +++ b/src/MindControl/InsufficientAllocatedMemoryException.cs @@ -0,0 +1,30 @@ +namespace MindControl; + +/// +/// Exception thrown by an when trying to reserve a memory range with a size exceeding +/// the largest contiguous unreserved space. +/// +public class InsufficientAllocatedMemoryException : Exception +{ + /// + /// Gets the requested size for the reservation attempt that caused the exception. + /// + public ulong RequestedSize { get; } + + /// + /// Gets the requested byte alignment for the reservation attempt that caused the exception. + /// + public uint? RequestedAlignment { get; } + + /// + /// Builds a new with the given parameters. + /// + /// Requested size for the reservation attempt. + /// Requested byte alignment for the reservation attempt. + public InsufficientAllocatedMemoryException(ulong requestedSize, uint? requestedAlignment) + : base($"The requested size of {requestedSize} bytes with {(requestedAlignment == null ? "no alignment" : $"an alignment of {requestedAlignment.Value}")} bytes exceeds the largest contiguous unreserved space of the {nameof(AllocatedRange)} instance. Consider allocating more space, reserving multiple smaller blocks, or letting the {nameof(ProcessMemory)} instance handle allocations by using addressless Write method signatures. Read the \"Allocating memory\" section in the documentation for more information.") + { + RequestedSize = requestedSize; + RequestedAlignment = requestedAlignment; + } +} \ No newline at end of file diff --git a/src/MindControl/MemoryRange.cs b/src/MindControl/MemoryRange.cs index 05118fe..3e69df4 100644 --- a/src/MindControl/MemoryRange.cs +++ b/src/MindControl/MemoryRange.cs @@ -3,10 +3,28 @@ /// /// Represents a range of memory addresses in a process. /// -/// Start address of the range. -/// End address of the range. -public readonly record struct MemoryRange(UIntPtr Start, UIntPtr End) +public readonly record struct MemoryRange { + /// Start address of the range. + public UIntPtr Start { get; init; } + + /// End address of the range. + public UIntPtr End { get; init; } + + /// + /// Builds a . + /// + /// Start address of the range. + /// End address of the range. + public MemoryRange(UIntPtr Start, UIntPtr End) + { + this.Start = Start; + this.End = End; + + if (Start.ToUInt64() < End.ToUInt64()) + throw new ArgumentException($"The start address of the memory range cannot be greater than the end address. If you are trying to build a range from a start address and a size, use the {nameof(FromStartAndSize)} static method instead."); + } + /// /// Creates a new memory range from a start address and a size. /// @@ -22,18 +40,89 @@ public static MemoryRange FromStartAndSize(UIntPtr start, ulong size) /// True if the address is within the memory range, false otherwise. public bool IsInRange(UIntPtr address) => address.ToUInt64() >= Start.ToUInt64() && address.ToUInt64() <= End.ToUInt64(); + + /// + /// Determines if the specified range is entirely contained within this range. + /// + /// Range to check. + /// True if the range is entirely contained within this range, false otherwise. + public bool Contains(MemoryRange range) + => range.Start.ToUInt64() >= Start.ToUInt64() && range.End.ToUInt64() <= End.ToUInt64(); /// - /// Throws an exception if the memory range is invalid. + /// Determines if the specified range overlaps with this range. /// - public void Validate() + /// Range to check. + /// True if the range overlaps with this range, false otherwise. + public bool Overlaps(MemoryRange range) + => Start.ToUInt64() <= range.End.ToUInt64() && range.Start.ToUInt64() <= End.ToUInt64(); + + /// + /// Returns the result of excluding the given range from this range. This can be seen as an XOR operation between + /// the two ranges. + /// This will result in no range when this range is entirely contained within the given range, two ranges when + /// the given range is in the middle of this range, or in other cases a single range that will be reduced by the + /// overlapping part (if any). + /// + /// The range to subtract from this range. + /// The resulting ranges. The resulting collection may be empty. + public IEnumerable Exclude(MemoryRange rangeToExclude) { - if (Start.ToUInt64() >= End.ToUInt64()) - throw new ArgumentException("The start address of the memory range cannot be greater than the end address."); + // This range is entirely contained in the range to subtract: no range left + if (rangeToExclude.Contains(this)) + return Array.Empty(); + + // No overlap between the two ranges: the original range is returned, untouched + if (!rangeToExclude.Overlaps(this)) + return new[] { this }; + + // There is an overlap: either one or two ranges will be returned, depending on the overlap + var results = new List(); + if (rangeToExclude.Start.ToUInt64() > Start.ToUInt64()) + results.Add(new MemoryRange(Start, rangeToExclude.Start - 1)); + + if (rangeToExclude.End.ToUInt64() < End.ToUInt64()) + results.Add(new MemoryRange(rangeToExclude.End + 1, End)); + + return results; + } + + /// + /// Returns a subset of this range, aligned to the specified byte alignment. + /// For example, a range of [2,9] aligned to 4 bytes will result in [4,8]. + /// + /// Alignment in bytes. Usually 4 for 32-bits processes, or 8 for 64-bits processes. + /// Indicates if the start of the range should be aligned. Defaults to true. + /// Indicates if the end of the range should be aligned. Defaults to true. + /// The aligned memory range. The returned range is always a subset of the range, or the range itself. + /// + public MemoryRange AlignedTo(uint alignment, bool alignStart = true, bool alignEnd = true) + { + if (!alignStart && !alignEnd) + return this; + + var start = Start.ToUInt64(); + var end = End.ToUInt64(); + + ulong alignedStart = alignStart ? start + (alignment - start % alignment) % alignment : start; + ulong alignedEnd = alignEnd ? end - end % alignment : end; + + return new MemoryRange((UIntPtr)alignedStart, (UIntPtr)alignedEnd); } /// /// Returns the size of the memory range. /// public ulong GetSize() => End.ToUInt64() - Start.ToUInt64() + 1; + + /// + /// Deconstructs the memory range into its start and end addresses. + /// + /// Start address of the range. + /// End address of the range. + public void Deconstruct(out UIntPtr start, out UIntPtr end) + { + start = Start; + end = End; + } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs new file mode 100644 index 0000000..600defe --- /dev/null +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -0,0 +1,39 @@ +namespace MindControl; + +// This partial class implements methods related to memory allocation. +public partial class ProcessMemory +{ + private readonly List _allocatedRanges = new(); + + /// + /// Gets the ranges allocated in this process. + /// Dispose a range to free the memory and remove it from this list. + /// + public IReadOnlyList AllocatedRanges => _allocatedRanges.AsReadOnly(); + + /// + /// Attempts to allocate a memory range of the given size within the process. + /// + /// Size of the memory range to allocate. + /// Specify this parameter to limit the allocation to a specific range of memory. + /// The allocated memory range. + public AllocatedRange Allocate(ulong size, MemoryRange? limitRange = null) + { + throw new NotImplementedException(); + } + + /// + /// Frees the memory allocated for the given range. + /// + /// Range to free. + /// This method is internal because it is designed to be called by . + /// Users would release memory by disposing ranges. + internal void Free(AllocatedRange range) + { + if (!_allocatedRanges.Contains(range)) + return; + + _allocatedRanges.Remove(range); + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs b/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs index 7b00ece..6b954b1 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs @@ -76,7 +76,6 @@ public IEnumerable FindBytes(string bytePattern, MemoryRange? range = n { var actualSettings = settings ?? new FindBytesSettings(); var actualRange = GetClampedMemoryRange(range); - actualRange.Validate(); actualSettings.Validate(); (byte[] bytePatternArray, byte[] maskArray) = ParseBytePattern(bytePattern); From b1d154a798d1b98b106cff675b74a0db06305311 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Tue, 16 Apr 2024 21:57:15 +0200 Subject: [PATCH 02/66] Unit tests for the new MemoryRange methods --- .../MindControl.csproj.DotSettings | 3 +- .../{ => Ranges}/AllocatedRange.cs | 0 src/MindControl/{ => Ranges}/MemoryRange.cs | 48 +++- .../RangeTests/MemoryRangeTest.cs | 254 ++++++++++++++++++ 4 files changed, 292 insertions(+), 13 deletions(-) rename src/MindControl/{ => Ranges}/AllocatedRange.cs (100%) rename src/MindControl/{ => Ranges}/MemoryRange.cs (76%) create mode 100644 test/MindControl.Test/RangeTests/MemoryRangeTest.cs diff --git a/src/MindControl/MindControl.csproj.DotSettings b/src/MindControl/MindControl.csproj.DotSettings index 6eb7ae3..9922787 100644 --- a/src/MindControl/MindControl.csproj.DotSettings +++ b/src/MindControl/MindControl.csproj.DotSettings @@ -1,2 +1,3 @@  - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/src/MindControl/AllocatedRange.cs b/src/MindControl/Ranges/AllocatedRange.cs similarity index 100% rename from src/MindControl/AllocatedRange.cs rename to src/MindControl/Ranges/AllocatedRange.cs diff --git a/src/MindControl/MemoryRange.cs b/src/MindControl/Ranges/MemoryRange.cs similarity index 76% rename from src/MindControl/MemoryRange.cs rename to src/MindControl/Ranges/MemoryRange.cs index 3e69df4..ec42459 100644 --- a/src/MindControl/MemoryRange.cs +++ b/src/MindControl/Ranges/MemoryRange.cs @@ -21,7 +21,7 @@ public MemoryRange(UIntPtr Start, UIntPtr End) this.Start = Start; this.End = End; - if (Start.ToUInt64() < End.ToUInt64()) + if (Start.ToUInt64() > End.ToUInt64()) throw new ArgumentException($"The start address of the memory range cannot be greater than the end address. If you are trying to build a range from a start address and a size, use the {nameof(FromStartAndSize)} static method instead."); } @@ -31,7 +31,12 @@ public MemoryRange(UIntPtr Start, UIntPtr End) /// Start address of the range. /// Size of the range in bytes. public static MemoryRange FromStartAndSize(UIntPtr start, ulong size) - => new(start, (UIntPtr)(start.ToUInt64() + size)); + { + if (size == 0) + throw new ArgumentException("The size of the memory range cannot be zero.", nameof(size)); + + return new MemoryRange(start, (UIntPtr)(start.ToUInt64() + size - 1)); + } /// /// Determines if the specified address is within the memory range. @@ -92,22 +97,26 @@ public IEnumerable Exclude(MemoryRange rangeToExclude) /// For example, a range of [2,9] aligned to 4 bytes will result in [4,8]. /// /// Alignment in bytes. Usually 4 for 32-bits processes, or 8 for 64-bits processes. - /// Indicates if the start of the range should be aligned. Defaults to true. - /// Indicates if the end of the range should be aligned. Defaults to true. + /// Alignment mode. Defines how the range should be aligned. Defaults to + /// . /// The aligned memory range. The returned range is always a subset of the range, or the range itself. /// - public MemoryRange AlignedTo(uint alignment, bool alignStart = true, bool alignEnd = true) + public MemoryRange AlignedTo(uint alignment, RangeAlignmentMode alignmentMode = RangeAlignmentMode.AlignBlock) { - if (!alignStart && !alignEnd) + if (alignment == 0) + throw new ArgumentException("The alignment value cannot be zero.", nameof(alignment)); + + if (alignmentMode == RangeAlignmentMode.None || alignment == 1) return this; + bool alignSize = alignmentMode == RangeAlignmentMode.AlignBlock; var start = Start.ToUInt64(); - var end = End.ToUInt64(); + ulong alignedStart = start + (alignment - start % alignment) % alignment; - ulong alignedStart = alignStart ? start + (alignment - start % alignment) % alignment : start; - ulong alignedEnd = alignEnd ? end - end % alignment : end; - - return new MemoryRange((UIntPtr)alignedStart, (UIntPtr)alignedEnd); + ulong size = End.ToUInt64() - alignedStart + 1; + ulong alignedSize = alignSize ? size - size % alignment : size; + + return new MemoryRange((UIntPtr)alignedStart, (UIntPtr)(alignedStart + alignedSize - 1)); } /// @@ -125,4 +134,19 @@ public void Deconstruct(out UIntPtr start, out UIntPtr end) start = Start; end = End; } -} \ No newline at end of file +} + +/// +/// Defines a byte alignment mode for memory ranges. +/// +public enum RangeAlignmentMode +{ + /// Do not align the range. + None, + + /// Align the start of the range, but not the size. + AlignStart, + + /// Align both the start and the size of the range. + AlignBlock +} diff --git a/test/MindControl.Test/RangeTests/MemoryRangeTest.cs b/test/MindControl.Test/RangeTests/MemoryRangeTest.cs new file mode 100644 index 0000000..e15dcc8 --- /dev/null +++ b/test/MindControl.Test/RangeTests/MemoryRangeTest.cs @@ -0,0 +1,254 @@ +using NUnit.Framework; + +namespace MindControl.Test.RangeTests; + +/// +/// Tests the class. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class MemoryRangeTest +{ + #region Constructor + + /// + /// Tests the constructor of the class with valid input. + /// It should set the start and end properties correctly. + /// + [Test] + public void ConstructorWithValidInputTest() + { + var range = new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1FFF)); + Assert.Multiple(() => + { + Assert.That(range.Start, Is.EqualTo(new UIntPtr(0x1000))); + Assert.That(range.End, Is.EqualTo(new UIntPtr(0x1FFF))); + }); + } + + /// + /// Tests the constructor of the class with a start address greater than the end address. + /// It should throw an . + /// + [Test] + public void ConstructorWithInvalidInputTest() + => Assert.That(() => new MemoryRange(new UIntPtr(0x2000), new UIntPtr(0x1000)), Throws.ArgumentException); + + #endregion + + #region FromStartAndSize + + /// + /// Tests the method with valid input. + /// It should return a with the correct start and end addresses. + /// + [Test] + public void FromStartAndSizeWithValidInputTest() + { + var range = MemoryRange.FromStartAndSize(new UIntPtr(0x1000), 0x1000); + Assert.Multiple(() => + { + Assert.That(range.Start, Is.EqualTo(new UIntPtr(0x1000))); + Assert.That(range.End, Is.EqualTo(new UIntPtr(0x1FFF))); + }); + } + + /// + /// Tests the method with a zero size. + /// It should throw an . + /// + [Test] + public void FromStartAndSizeWithZeroSizeTest() + => Assert.That(() => MemoryRange.FromStartAndSize(new UIntPtr(0x1000), 0), Throws.ArgumentException); + + #endregion + + #region GetSize + + /// + /// Tests the method. + /// It should return the size of the range. + /// + [Test] + public void GetSizeTest() + { + var range = new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1FFF)); + Assert.That(range.GetSize(), Is.EqualTo(0x1000)); + } + + #endregion + + #region IsInRange + + /// + /// Tests the method with specified addresses and ranges. + /// It should return the specified expected value. + /// + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1000, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1FFF, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x0FFF, ExpectedResult = false)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x2000, ExpectedResult = false)] + [TestCase((ulong)0x1000, (ulong)0x1000, (ulong)0x1000, ExpectedResult = true)] + public bool IsInRangeTest(ulong start, ulong end, ulong address) + { + var range = new MemoryRange(new UIntPtr(start), new UIntPtr(end)); + return range.IsInRange(new UIntPtr(address)); + } + + #endregion + + #region Contains + + /// + /// Tests the method with specified ranges. + /// It should return the specified expected value. + /// + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1001, (ulong)0x1FFE, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1000, (ulong)0x1FFF, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1000, (ulong)0x1FFE, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1001, (ulong)0x1FFF, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x0FFF, (ulong)0x1FFF, ExpectedResult = false)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1000, (ulong)0x2000, ExpectedResult = false)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x2000, (ulong)0x2FFF, ExpectedResult = false)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x0000, (ulong)0x0FFF, ExpectedResult = false)] + public bool ContainsTest(ulong start, ulong end, ulong otherStart, ulong otherEnd) + { + var range = new MemoryRange(new UIntPtr(start), new UIntPtr(end)); + var otherRange = new MemoryRange(new UIntPtr(otherStart), new UIntPtr(otherEnd)); + return range.Contains(otherRange); + } + + #endregion + + #region Overlaps + + /// + /// Tests the method with specified ranges. + /// It should return the specified expected value. + /// + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1001, (ulong)0x1FFE, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1000, (ulong)0x1FFF, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1000, (ulong)0x1FFE, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1001, (ulong)0x1FFF, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x0FFF, (ulong)0x1FFF, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1000, (ulong)0x2000, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x0FFF, (ulong)0x2000, ExpectedResult = true)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x2000, (ulong)0x2FFF, ExpectedResult = false)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x0000, (ulong)0x0FFF, ExpectedResult = false)] + public bool OverlapsTest(ulong start, ulong end, ulong otherStart, ulong otherEnd) + { + var range = new MemoryRange(new UIntPtr(start), new UIntPtr(end)); + var otherRange = new MemoryRange(new UIntPtr(otherStart), new UIntPtr(otherEnd)); + + // Overlaps is commutative, so we check both ways + return range.Overlaps(otherRange) && otherRange.Overlaps(range); + } + + #endregion + + #region Exclude + + /// + /// Describes a test case for the method. + /// + public record struct ExcludeTestCase(ulong Start, ulong End, + ulong OtherStart, ulong OtherEnd, MemoryRange[] ExpectedRanges); + + private static ExcludeTestCase[] _excludeTestCases = { + new(0x1000, 0x1FFF, 0x1100, 0x11FF, new[] { + new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x10FF)), + new MemoryRange(new UIntPtr(0x1200), new UIntPtr(0x1FFF))}), + + new(0x1000, 0x1FFF, 0x1000, 0x11FF, new[] { + new MemoryRange(new UIntPtr(0x1200), new UIntPtr(0x1FFF))}), + + new(0x1000, 0x1FFF, 0x0F00, 0x11FF, new[] { + new MemoryRange(new UIntPtr(0x1200), new UIntPtr(0x1FFF))}), + + new(0x1000, 0x1FFF, 0x1F00, 0x1FFF, new[] { + new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1EFF))}), + + new(0x1000, 0x1FFF, 0x1F00, 0x2100, new[] { + new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1EFF))}), + + new(0x1000, 0x1FFF, 0x1F00, 0x1FFF, new[] { + new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1EFF))}), + + new(0x1000, 0x1FFF, 0x0100, 0x0200, new[] { + new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1FFF))}), + + new(0x1000, 0x1FFF, 0x2100, 0x2200, new[] { + new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1FFF))}), + + new(0x1000, 0x1FFF, 0x0100, 0x0200, new[] { + new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1FFF))}), + + new(0x1000, 0x1FFF, 0x1000, 0x1FFF, Array.Empty()), + + new(0x1000, 0x1FFF, 0x0100, 0x2200, Array.Empty()), + }; + + /// + /// Tests the method with specified ranges. + /// It should return the specified expected value. + /// + [TestCaseSource(nameof(_excludeTestCases))] + public void ExcludeTest(ExcludeTestCase testCase) + { + var range = new MemoryRange(new UIntPtr(testCase.Start), new UIntPtr(testCase.End)); + var otherRange = new MemoryRange(new UIntPtr(testCase.OtherStart), new UIntPtr(testCase.OtherEnd)); + var results = range.Exclude(otherRange).ToArray(); + Assert.That(results, Is.EquivalentTo(testCase.ExpectedRanges)); + } + + #endregion + + #region AlignedTo + + /// + /// Describes a test case for the method. + /// + public record struct AlignedToTestCase(ulong Start, ulong End, uint Alignment, + RangeAlignmentMode AlignmentMode, ulong ExpectedStart, ulong ExpectedEnd); + + private static AlignedToTestCase[] _alignedToTestCases = { + new(0x1000, 0x1FFF, 4, RangeAlignmentMode.AlignBlock, 0x1000, 0x1FFF), + new(0x1000, 0x1FFF, 4, RangeAlignmentMode.AlignStart, 0x1000, 0x1FFF), + new(0x1000, 0x1FFF, 4, RangeAlignmentMode.None, 0x1000, 0x1FFF), + + new(0x1000, 0x1FFF, 8, RangeAlignmentMode.AlignBlock, 0x1000, 0x1FFF), + new(0x1000, 0x1FFF, 8, RangeAlignmentMode.AlignStart, 0x1000, 0x1FFF), + new(0x1000, 0x1FFF, 8, RangeAlignmentMode.None, 0x1000, 0x1FFF), + + new(0x1001, 0x1FFE, 4, RangeAlignmentMode.AlignBlock, 0x1004, 0x1FFB), + new(0x1001, 0x1FFE, 4, RangeAlignmentMode.AlignStart, 0x1004, 0x1FFE), + new(0x1001, 0x1FFE, 4, RangeAlignmentMode.None, 0x1001, 0x1FFE), + + new(0x1001, 0x1FFE, 8, RangeAlignmentMode.AlignBlock, 0x1008, 0x1FF7), + new(0x1001, 0x1FFE, 8, RangeAlignmentMode.AlignStart, 0x1008, 0x1FFE), + new(0x1001, 0x1FFE, 8, RangeAlignmentMode.None, 0x1001, 0x1FFE) + }; + + /// + /// Tests the method with specified alignment. + /// It should return the expected range. + /// + [TestCaseSource(nameof(_alignedToTestCases))] + public void AlignedToTest(AlignedToTestCase testCase) + { + var range = new MemoryRange(new UIntPtr(testCase.Start), new UIntPtr(testCase.End)); + var alignedRange = range.AlignedTo(testCase.Alignment, testCase.AlignmentMode); + var expectedRange = new MemoryRange(new UIntPtr(testCase.ExpectedStart), new UIntPtr(testCase.ExpectedEnd)); + Assert.That(alignedRange, Is.EqualTo(expectedRange)); + } + + /// + /// Tests the method with zero alignment. + /// It should throw an . + /// + [Test] + public void AlignedToWithZeroAlignmentTest() + => Assert.That(() => new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1FFF)).AlignedTo(0), + Throws.ArgumentException); + + #endregion +} \ No newline at end of file From b230d67ecd0f4d791eda6aa4ffb61939d3b031cd Mon Sep 17 00:00:00 2001 From: Doublevil Date: Wed, 1 May 2024 22:05:17 +0200 Subject: [PATCH 03/66] Functional Allocate implementation --- .../Native/IOperatingSystemService.cs | 22 +++++ src/MindControl/Native/MemoryRangeMetadata.cs | 5 ++ src/MindControl/Native/Win32Service.cs | 25 +++++- .../ProcessMemory/ProcessMemory.Allocation.cs | 82 ++++++++++++++++++- .../ProcessMemory/ProcessMemory.cs | 13 +++ src/MindControl/Ranges/AllocatedRange.cs | 4 +- src/MindControl/Ranges/MemoryRange.cs | 22 ++++- .../ProcessMemoryAllocationTest.cs | 29 +++++++ .../RangeTests/AllocatedRangeTest.cs | 16 ++++ .../RangeTests/MemoryRangeTest.cs | 68 +++++++++++---- 10 files changed, 261 insertions(+), 25 deletions(-) create mode 100644 test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs create mode 100644 test/MindControl.Test/RangeTests/AllocatedRangeTest.cs diff --git a/src/MindControl/Native/IOperatingSystemService.cs b/src/MindControl/Native/IOperatingSystemService.cs index b49f6d8..893ce9f 100644 --- a/src/MindControl/Native/IOperatingSystemService.cs +++ b/src/MindControl/Native/IOperatingSystemService.cs @@ -66,6 +66,18 @@ MemoryProtection ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bits, UIntPtr AllocateMemory(IntPtr processHandle, int size, MemoryAllocationType allocationType, MemoryProtection protection); + /// + /// Allocates memory in the specified process at the specified address. + /// + /// Handle of the target process. + /// Address where the memory will be allocated. + /// Size in bytes of the memory to allocate. + /// Type of memory allocation. + /// Protection flags of the memory to allocate. + /// A pointer to the start of the allocated memory. + UIntPtr AllocateMemory(IntPtr processHandle, UIntPtr address, int size, MemoryAllocationType allocationType, + MemoryProtection protection); + /// /// Gets the address of the function used to load a library in the current process. /// @@ -116,4 +128,14 @@ UIntPtr AllocateMemory(IntPtr processHandle, int size, MemoryAllocationType allo /// A boolean indicating if the target process is 64 bits or not. /// If left null, the method will automatically determine the bitness of the process. MemoryRangeMetadata GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress, bool? is64Bits = null); + + /// + /// Gets the allocation granularity (minimal allocation size) of the system. + /// + uint GetAllocationGranularity(); + + /// + /// Gets the page size of the system. + /// + uint GetPageSize(); } diff --git a/src/MindControl/Native/MemoryRangeMetadata.cs b/src/MindControl/Native/MemoryRangeMetadata.cs index 00e6c35..3f2b587 100644 --- a/src/MindControl/Native/MemoryRangeMetadata.cs +++ b/src/MindControl/Native/MemoryRangeMetadata.cs @@ -20,6 +20,11 @@ public struct MemoryRangeMetadata /// public bool IsCommitted { get; init; } + /// + /// Gets a boolean indicating if the memory is free. + /// + public bool IsFree { get; init; } + /// /// Gets a boolean indicating if the memory is guarded or marked for no access. /// diff --git a/src/MindControl/Native/Win32Service.cs b/src/MindControl/Native/Win32Service.cs index 8ea7ea6..5d1e0f0 100644 --- a/src/MindControl/Native/Win32Service.cs +++ b/src/MindControl/Native/Win32Service.cs @@ -147,6 +147,18 @@ public void WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, byte /// Protection flags of the memory to allocate. /// A pointer to the start of the allocated memory. public UIntPtr AllocateMemory(IntPtr processHandle, int size, MemoryAllocationType allocationType, + MemoryProtection protection) => AllocateMemory(processHandle, UIntPtr.Zero, size, allocationType, protection); + + /// + /// Allocates memory in the specified process at the specified address. + /// + /// Handle of the target process. + /// Address where the memory will be allocated. + /// Size in bytes of the memory to allocate. + /// Type of memory allocation. + /// Protection flags of the memory to allocate. + /// A pointer to the start of the allocated memory. + public UIntPtr AllocateMemory(IntPtr processHandle, UIntPtr address, int size, MemoryAllocationType allocationType, MemoryProtection protection) { if (processHandle == IntPtr.Zero) @@ -154,7 +166,7 @@ public UIntPtr AllocateMemory(IntPtr processHandle, int size, MemoryAllocationTy if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size),"The size to allocate must be strictly positive."); - var result = VirtualAllocEx(processHandle, UIntPtr.Zero, (uint)size, (uint)allocationType, (uint)protection); + var result = VirtualAllocEx(processHandle, address, (uint)size, (uint)allocationType, (uint)protection); if (result == UIntPtr.Zero) throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. @@ -281,6 +293,16 @@ public MemoryRange GetFullMemoryRange() return new MemoryRange(systemInfo.MinimumApplicationAddress, systemInfo.MaximumApplicationAddress); } + /// + /// Gets the allocation granularity (minimal allocation size) of the system. + /// + public uint GetAllocationGranularity() => GetSystemInfo().AllocationGranularity; + + /// + /// Gets the page size of the system. + /// + public uint GetPageSize() => GetSystemInfo().PageSize; + /// /// Gets the metadata of a memory region in the virtual address space of a process. /// @@ -324,6 +346,7 @@ public MemoryRangeMetadata GetRegionMetadata(IntPtr processHandle, UIntPtr baseA StartAddress = memoryBasicInformation.BaseAddress, Size = memoryBasicInformation.RegionSize, IsCommitted = memoryBasicInformation.State == MemoryState.Commit, + IsFree = memoryBasicInformation.State == MemoryState.Free, IsProtected = memoryBasicInformation.Protect.HasFlag(MemoryProtection.PageGuard) || memoryBasicInformation.Protect.HasFlag(MemoryProtection.NoAccess), IsMapped = memoryBasicInformation.Type == PageType.Mapped, diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index 600defe..af2f558 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -1,4 +1,6 @@ -namespace MindControl; +using MindControl.Native; + +namespace MindControl; // This partial class implements methods related to memory allocation. public partial class ProcessMemory @@ -15,11 +17,25 @@ public partial class ProcessMemory /// Attempts to allocate a memory range of the given size within the process. /// /// Size of the memory range to allocate. + /// Determines if the memory range can be used to store executable code. /// Specify this parameter to limit the allocation to a specific range of memory. /// The allocated memory range. - public AllocatedRange Allocate(ulong size, MemoryRange? limitRange = null) + public AllocatedRange Allocate(ulong size, bool forExecutableCode, MemoryRange? limitRange = null) { - throw new NotImplementedException(); + // Find a free memory range that satisfies the size needed + var range = FindFreeMemory(size, limitRange); + if (range == null) + throw new InvalidOperationException("No suitable free memory range was found."); + + // Allocate the memory range + _osService.AllocateMemory(_processHandle, range.Value.Start, (int)range.Value.GetSize(), + MemoryAllocationType.Commit | MemoryAllocationType.Reserve, + forExecutableCode ? MemoryProtection.ExecuteReadWrite : MemoryProtection.ReadWrite); + + // Add the range to the list of allocated ranges and return it + var allocatedRange = new AllocatedRange(range.Value, forExecutableCode, this); + _allocatedRanges.Add(allocatedRange); + return allocatedRange; } /// @@ -34,6 +50,64 @@ internal void Free(AllocatedRange range) return; _allocatedRanges.Remove(range); - throw new NotImplementedException(); + _osService.ReleaseMemory(_processHandle, range.Range.Start); + } + + /// + /// Finds a free memory range that satisfies the size needed. + /// + /// Size of the memory range needed. + /// Specify this parameter to limit the search to a specific range of memory. + /// If left null (default), the entire process memory will be searched. + /// The start address of the free memory range, or null if no suitable range was found. + private MemoryRange? FindFreeMemory(ulong sizeNeeded, MemoryRange? limitRange = null) + { + var maxRange = _osService.GetFullMemoryRange(); + var actualRange = limitRange == null ? maxRange : maxRange.Intersect(limitRange.Value); + + // If the given range is not within the process applicative memory, return null + if (actualRange == null) + return null; + + // Compute the minimum multiple of the system page size that can fit the size needed + // This will be the maximum size that we are going to allocate + uint pageSize = _osService.GetPageSize(); + uint minFittingPageSize = (uint)(sizeNeeded / pageSize + 1) * pageSize; + + // Browse through regions in the memory range to find the first one that satisfies the size needed + var nextAddress = actualRange.Value.Start; + MemoryRange? freeRange = null; + MemoryRangeMetadata currentMetadata; + while (nextAddress.ToUInt64() < actualRange.Value.End.ToUInt64() + && (currentMetadata = _osService.GetRegionMetadata(_processHandle, nextAddress, _is64Bits)) + .Size.ToUInt64() > 0) + { + nextAddress = (UIntPtr)(nextAddress.ToUInt64() + currentMetadata.Size.ToUInt64()); + + // If the current region cannot be used, reinitialize the current free range and keep iterating + if (!currentMetadata.IsFree) + { + freeRange = null; + continue; + } + + // Build a range with the current region + // Start from the start of the current free range if it's not null, so that we can have ranges that span + // across multiple regions. + freeRange = new MemoryRange(freeRange?.Start ?? currentMetadata.StartAddress, + (UIntPtr)(currentMetadata.StartAddress.ToUInt64() + currentMetadata.Size.ToUInt64())); + + if (freeRange.Value.GetSize() >= sizeNeeded) + { + // The free range is large enough. + // If the free range is larger than the size needed, we will allocate the minimum multiple of the + // system page size that can fit the requested size. + ulong neededSize = Math.Min(freeRange.Value.GetSize(), minFittingPageSize); + return MemoryRange.FromStartAndSize(freeRange.Value.Start, neededSize); + } + } + + // If we reached the end of the memory range and didn't find a suitable free range, return null + return null; } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.cs b/src/MindControl/ProcessMemory/ProcessMemory.cs index 140cd1d..17d7782 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.cs @@ -174,6 +174,19 @@ private void Detach() if (IsAttached) { IsAttached = false; + for (int i = _allocatedRanges.Count - 1; i >= 0; i--) + { + try + { + _allocatedRanges[i].Dispose(); + } + catch (Exception) + { + // Just skip. We probably lost the process and thus cannot do anything with it anymore. + _allocatedRanges.RemoveAt(i); + } + } + _process.Exited -= OnProcessExited; ProcessDetached?.Invoke(this, EventArgs.Empty); } diff --git a/src/MindControl/Ranges/AllocatedRange.cs b/src/MindControl/Ranges/AllocatedRange.cs index 246cc78..20ffe8f 100644 --- a/src/MindControl/Ranges/AllocatedRange.cs +++ b/src/MindControl/Ranges/AllocatedRange.cs @@ -137,8 +137,8 @@ private IEnumerable GetFreeRanges() var matchingRange = GetFreeRanges() .Select(r => byteAlignment == null ? r : r.AlignedTo(byteAlignment.Value)) - .OrderBy(r => r.Start.ToUInt64()) - .Cast() + .Where(r => r.HasValue) + .OrderBy(r => r!.Value.Start.ToUInt64()) .FirstOrDefault(r => r?.GetSize() > size); if (matchingRange == null) diff --git a/src/MindControl/Ranges/MemoryRange.cs b/src/MindControl/Ranges/MemoryRange.cs index ec42459..b87ce5e 100644 --- a/src/MindControl/Ranges/MemoryRange.cs +++ b/src/MindControl/Ranges/MemoryRange.cs @@ -62,6 +62,18 @@ public bool Contains(MemoryRange range) public bool Overlaps(MemoryRange range) => Start.ToUInt64() <= range.End.ToUInt64() && range.Start.ToUInt64() <= End.ToUInt64(); + /// + /// Obtains the intersection of this range with the given range, which is the range that is common to both. + /// + /// The range to intersect with. + /// The intersection of the two ranges, or null if there is no intersection. + public MemoryRange? Intersect(MemoryRange otherRange) + { + ulong start = Math.Max(Start.ToUInt64(), otherRange.Start.ToUInt64()); + ulong end = Math.Min(End.ToUInt64(), otherRange.End.ToUInt64()); + return start <= end ? new MemoryRange((UIntPtr)start, (UIntPtr)end) : null; + } + /// /// Returns the result of excluding the given range from this range. This can be seen as an XOR operation between /// the two ranges. @@ -100,8 +112,8 @@ public IEnumerable Exclude(MemoryRange rangeToExclude) /// Alignment mode. Defines how the range should be aligned. Defaults to /// . /// The aligned memory range. The returned range is always a subset of the range, or the range itself. - /// - public MemoryRange AlignedTo(uint alignment, RangeAlignmentMode alignmentMode = RangeAlignmentMode.AlignBlock) + /// If the aligned memory range cannot fit in the original range, returns null. + public MemoryRange? AlignedTo(uint alignment, RangeAlignmentMode alignmentMode = RangeAlignmentMode.AlignBlock) { if (alignment == 0) throw new ArgumentException("The alignment value cannot be zero.", nameof(alignment)); @@ -116,7 +128,11 @@ public MemoryRange AlignedTo(uint alignment, RangeAlignmentMode alignmentMode = ulong size = End.ToUInt64() - alignedStart + 1; ulong alignedSize = alignSize ? size - size % alignment : size; - return new MemoryRange((UIntPtr)alignedStart, (UIntPtr)(alignedStart + alignedSize - 1)); + ulong end = alignedStart + alignedSize - 1; + if (alignedStart > End.ToUInt64()) + return null; + + return new MemoryRange((UIntPtr)alignedStart, (UIntPtr)end); } /// diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs new file mode 100644 index 0000000..504674c --- /dev/null +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -0,0 +1,29 @@ +using NUnit.Framework; + +namespace MindControl.Test.ProcessMemoryTests; + +/// +/// Tests the features of the class related to memory allocation. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryAllocationTest : ProcessMemoryTest +{ + /// + /// Tests the method. + /// Performs a simple allocation and verifies that it is writable and the range is in the list of allocated ranges. + /// + [Test] + public void AllocateTest() + { + var range = TestProcessMemory!.Allocate(0x1000, false); + + // To check that the memory is writable, we will write a byte array to the allocated range. + // We will use the WriteBytes method rather than Store, because Store is built on top of Allocate and we only + // want to test the allocation here. + TestProcessMemory.WriteBytes(range.Range.Start, new byte[0x1000]); + + Assert.That(range, Is.Not.Null); + Assert.That(range.Range.GetSize(), Is.AtLeast(0x1000)); + Assert.That(TestProcessMemory!.AllocatedRanges, Has.Member(range)); + } +} \ No newline at end of file diff --git a/test/MindControl.Test/RangeTests/AllocatedRangeTest.cs b/test/MindControl.Test/RangeTests/AllocatedRangeTest.cs new file mode 100644 index 0000000..fe0c8cf --- /dev/null +++ b/test/MindControl.Test/RangeTests/AllocatedRangeTest.cs @@ -0,0 +1,16 @@ +using MindControl.Test.ProcessMemoryTests; +using NUnit.Framework; + +namespace MindControl.Test.RangeTests; + +/// +/// Tests the class. +/// Because this class is strongly bound to a ProcessMemory, we have to use the +/// method to create instances, so the tests below will use an actual instance of and +/// depend on that method. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class AllocatedRangeTest : ProcessMemoryTest +{ + +} diff --git a/test/MindControl.Test/RangeTests/MemoryRangeTest.cs b/test/MindControl.Test/RangeTests/MemoryRangeTest.cs index e15dcc8..b5a4b70 100644 --- a/test/MindControl.Test/RangeTests/MemoryRangeTest.cs +++ b/test/MindControl.Test/RangeTests/MemoryRangeTest.cs @@ -145,6 +145,43 @@ public bool OverlapsTest(ulong start, ulong end, ulong otherStart, ulong otherEn #endregion + #region Intersect + + /// + /// Describes a test case for the method. + /// + public record struct IntersectTestCase(ulong Start, ulong End, ulong OtherStart, ulong OtherEnd, + MemoryRange? ExpectedResult); + + private static IntersectTestCase[] _intersectTestCases = + { + new(0x1000, 0x1FFF, 0x1001, 0x1FFE, new MemoryRange(new UIntPtr(0x1001), new UIntPtr(0x1FFE))), + new(0x1000, 0x1FFF, 0x1000, 0x1FFF, new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1FFF))), + new(0x0FFF, 0x1000, 0x1000, 0x1FFF, new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1000))), + new(0x1FFF, 0x2000, 0x1000, 0x1FFF, new MemoryRange(new UIntPtr(0x1FFF), new UIntPtr(0x1FFF))), + new(0x1000, 0x1FFF, 0x2000, 0x2FFF, null), + new(0x1000, 0x1FFF, 0x0000, 0x0FFF, null) + }; + + /// + /// Tests the method with specified ranges. + /// It should return the specified expected value. + /// + [TestCaseSource(nameof(_intersectTestCases))] + public void IntersectTest(IntersectTestCase testCase) + { + var range = new MemoryRange(new UIntPtr(testCase.Start), new UIntPtr(testCase.End)); + var otherRange = new MemoryRange(new UIntPtr(testCase.OtherStart), new UIntPtr(testCase.OtherEnd)); + + // We check both ways because Intersect is commutative + var result = range.Intersect(otherRange); + var result2 = otherRange.Intersect(range); + Assert.That(result, Is.EqualTo(testCase.ExpectedResult)); + Assert.That(result2, Is.EqualTo(testCase.ExpectedResult)); + } + + #endregion + #region Exclude /// @@ -208,24 +245,26 @@ public void ExcludeTest(ExcludeTestCase testCase) /// Describes a test case for the method. /// public record struct AlignedToTestCase(ulong Start, ulong End, uint Alignment, - RangeAlignmentMode AlignmentMode, ulong ExpectedStart, ulong ExpectedEnd); + RangeAlignmentMode AlignmentMode, MemoryRange? ExpectedResult); private static AlignedToTestCase[] _alignedToTestCases = { - new(0x1000, 0x1FFF, 4, RangeAlignmentMode.AlignBlock, 0x1000, 0x1FFF), - new(0x1000, 0x1FFF, 4, RangeAlignmentMode.AlignStart, 0x1000, 0x1FFF), - new(0x1000, 0x1FFF, 4, RangeAlignmentMode.None, 0x1000, 0x1FFF), + new(0x1000, 0x1FFF, 4, RangeAlignmentMode.AlignBlock, new MemoryRange((UIntPtr)0x1000, (UIntPtr)0x1FFF)), + new(0x1000, 0x1FFF, 4, RangeAlignmentMode.AlignStart, new MemoryRange((UIntPtr)0x1000, (UIntPtr)0x1FFF)), + new(0x1000, 0x1FFF, 4, RangeAlignmentMode.None, new MemoryRange((UIntPtr)0x1000, (UIntPtr)0x1FFF)), + + new(0x1000, 0x1FFF, 8, RangeAlignmentMode.AlignBlock, new MemoryRange((UIntPtr)0x1000, (UIntPtr)0x1FFF)), + new(0x1000, 0x1FFF, 8, RangeAlignmentMode.AlignStart, new MemoryRange((UIntPtr)0x1000, (UIntPtr)0x1FFF)), + new(0x1000, 0x1FFF, 8, RangeAlignmentMode.None, new MemoryRange((UIntPtr)0x1000, (UIntPtr)0x1FFF)), - new(0x1000, 0x1FFF, 8, RangeAlignmentMode.AlignBlock, 0x1000, 0x1FFF), - new(0x1000, 0x1FFF, 8, RangeAlignmentMode.AlignStart, 0x1000, 0x1FFF), - new(0x1000, 0x1FFF, 8, RangeAlignmentMode.None, 0x1000, 0x1FFF), + new(0x1001, 0x1FFE, 4, RangeAlignmentMode.AlignBlock, new MemoryRange((UIntPtr)0x1004, (UIntPtr)0x1FFB)), + new(0x1001, 0x1FFE, 4, RangeAlignmentMode.AlignStart, new MemoryRange((UIntPtr)0x1004, (UIntPtr)0x1FFE)), + new(0x1001, 0x1FFE, 4, RangeAlignmentMode.None, new MemoryRange((UIntPtr)0x1001, (UIntPtr)0x1FFE)), - new(0x1001, 0x1FFE, 4, RangeAlignmentMode.AlignBlock, 0x1004, 0x1FFB), - new(0x1001, 0x1FFE, 4, RangeAlignmentMode.AlignStart, 0x1004, 0x1FFE), - new(0x1001, 0x1FFE, 4, RangeAlignmentMode.None, 0x1001, 0x1FFE), + new(0x1001, 0x1FFE, 8, RangeAlignmentMode.AlignBlock, new MemoryRange((UIntPtr)0x1008, (UIntPtr)0x1FF7)), + new(0x1001, 0x1FFE, 8, RangeAlignmentMode.AlignStart, new MemoryRange((UIntPtr)0x1008, (UIntPtr)0x1FFE)), + new(0x1001, 0x1FFE, 8, RangeAlignmentMode.None, new MemoryRange((UIntPtr)0x1001, (UIntPtr)0x1FFE)), - new(0x1001, 0x1FFE, 8, RangeAlignmentMode.AlignBlock, 0x1008, 0x1FF7), - new(0x1001, 0x1FFE, 8, RangeAlignmentMode.AlignStart, 0x1008, 0x1FFE), - new(0x1001, 0x1FFE, 8, RangeAlignmentMode.None, 0x1001, 0x1FFE) + new(0x1000, 0x1FFF, 0x2000, RangeAlignmentMode.AlignBlock, null) }; /// @@ -237,8 +276,7 @@ public void AlignedToTest(AlignedToTestCase testCase) { var range = new MemoryRange(new UIntPtr(testCase.Start), new UIntPtr(testCase.End)); var alignedRange = range.AlignedTo(testCase.Alignment, testCase.AlignmentMode); - var expectedRange = new MemoryRange(new UIntPtr(testCase.ExpectedStart), new UIntPtr(testCase.ExpectedEnd)); - Assert.That(alignedRange, Is.EqualTo(expectedRange)); + Assert.That(alignedRange, Is.EqualTo(testCase.ExpectedResult)); } /// From 9c712743a1848111f4d9c739a23468821da61835 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 4 May 2024 17:19:50 +0200 Subject: [PATCH 04/66] New Store methods and partial unit testing for allocations --- .../Internal/ConversionExtensions.cs | 50 ++++++++ .../ProcessMemory/ProcessMemory.Allocation.cs | 110 +++++++++++++++-- .../ProcessMemory/ProcessMemory.Write.cs | 5 +- src/MindControl/Ranges/AllocatedRange.cs | 4 +- .../ProcessMemoryAllocationTest.cs | 111 ++++++++++++++++++ .../ProcessMemoryWriteTest.cs | 20 +++- .../RangeTests/AllocatedRangeTest.cs | 88 +++++++++++++- 7 files changed, 372 insertions(+), 16 deletions(-) diff --git a/src/MindControl/Internal/ConversionExtensions.cs b/src/MindControl/Internal/ConversionExtensions.cs index 7f21036..338c259 100644 --- a/src/MindControl/Internal/ConversionExtensions.cs +++ b/src/MindControl/Internal/ConversionExtensions.cs @@ -1,5 +1,6 @@ using System.Numerics; using System.Runtime.InteropServices; +using System.Runtime.Serialization.Formatters.Binary; namespace MindControl.Internal; @@ -59,4 +60,53 @@ public static byte[] ToBytes(this IntPtr pointer, bool is64) return IntPtr.Size == 4 ? (UIntPtr)(uint)value : (UIntPtr)(ulong)value; } + + /// + /// Converts the given structure to an array of bytes. + /// + /// Value to convert. Must be a value type. + /// The array of bytes representing the value. + private static byte[] StructToBytes(object value) + { + // Note: this code only works with structs. + // Do not try this at home with reference types! + + int size = Marshal.SizeOf(value); + var arr = new byte[size]; + + var ptr = Marshal.AllocHGlobal(size); + try + { + Marshal.StructureToPtr(value, ptr, true); + Marshal.Copy(ptr, arr, 0, size); + } + finally + { + Marshal.FreeHGlobal(ptr); + } + + return arr; + } + + /// + /// Converts the object to an array of bytes. + /// + /// Value to convert. + /// The array of bytes representing the value. + public static byte[] ToBytes(this object? value) + { + switch (value) + { + case null: + throw new ArgumentNullException(nameof(value)); + case byte[] bytes: + return bytes; + } + + // For now, we will only handle structs. + if (value is not ValueType) + throw new ArgumentException($"The value must be a value type. {value.GetType().Name}", nameof(value)); + + return StructToBytes(value); + } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index af2f558..275f21e 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -1,4 +1,5 @@ -using MindControl.Native; +using MindControl.Internal; +using MindControl.Native; namespace MindControl; @@ -22,16 +23,15 @@ public partial class ProcessMemory /// The allocated memory range. public AllocatedRange Allocate(ulong size, bool forExecutableCode, MemoryRange? limitRange = null) { + if (size == 0) + throw new ArgumentException("The size of the memory range to allocate must be greater than zero.", + nameof(size)); + // Find a free memory range that satisfies the size needed - var range = FindFreeMemory(size, limitRange); + var range = FindAndAllocateFreeMemory(size, forExecutableCode, limitRange); if (range == null) throw new InvalidOperationException("No suitable free memory range was found."); - // Allocate the memory range - _osService.AllocateMemory(_processHandle, range.Value.Start, (int)range.Value.GetSize(), - MemoryAllocationType.Commit | MemoryAllocationType.Reserve, - forExecutableCode ? MemoryProtection.ExecuteReadWrite : MemoryProtection.ReadWrite); - // Add the range to the list of allocated ranges and return it var allocatedRange = new AllocatedRange(range.Value, forExecutableCode, this); _allocatedRanges.Add(allocatedRange); @@ -54,13 +54,15 @@ internal void Free(AllocatedRange range) } /// - /// Finds a free memory range that satisfies the size needed. + /// Finds a free memory range that satisfies the size needed and performs the allocation. /// /// Size of the memory range needed. + /// Set to true to allocate a memory block with execute permissions. /// Specify this parameter to limit the search to a specific range of memory. /// If left null (default), the entire process memory will be searched. /// The start address of the free memory range, or null if no suitable range was found. - private MemoryRange? FindFreeMemory(ulong sizeNeeded, MemoryRange? limitRange = null) + private MemoryRange? FindAndAllocateFreeMemory(ulong sizeNeeded, bool forExecutableCode, + MemoryRange? limitRange = null) { var maxRange = _osService.GetFullMemoryRange(); var actualRange = limitRange == null ? maxRange : maxRange.Intersect(limitRange.Value); @@ -72,7 +74,8 @@ internal void Free(AllocatedRange range) // Compute the minimum multiple of the system page size that can fit the size needed // This will be the maximum size that we are going to allocate uint pageSize = _osService.GetPageSize(); - uint minFittingPageSize = (uint)(sizeNeeded / pageSize + 1) * pageSize; + bool isDirectMultiple = sizeNeeded % pageSize == 0; + uint minFittingPageSize = (uint)(sizeNeeded / pageSize + (isDirectMultiple ? (ulong)0 : 1)) * pageSize; // Browse through regions in the memory range to find the first one that satisfies the size needed var nextAddress = actualRange.Value.Start; @@ -103,11 +106,96 @@ internal void Free(AllocatedRange range) // If the free range is larger than the size needed, we will allocate the minimum multiple of the // system page size that can fit the requested size. ulong neededSize = Math.Min(freeRange.Value.GetSize(), minFittingPageSize); - return MemoryRange.FromStartAndSize(freeRange.Value.Start, neededSize); + var finalRange = MemoryRange.FromStartAndSize(freeRange.Value.Start, neededSize); + + // Even if they are free, some regions cannot be allocated. + // The only way to know if a region can be allocated is to try to allocate it. + try + { + _osService.AllocateMemory(_processHandle, finalRange.Start, (int)finalRange.GetSize(), + MemoryAllocationType.Commit | MemoryAllocationType.Reserve, + forExecutableCode ? MemoryProtection.ExecuteReadWrite : MemoryProtection.ReadWrite); + return finalRange; + } + catch + { + // The allocation failed. Reset the current range and keep iterating. + freeRange = null; + continue; + } } } // If we reached the end of the memory range and didn't find a suitable free range, return null return null; } + + #region Store + + /// + /// Reserves a range of memory of the given size. If no suitable range is found, a new range is allocated, and + /// a reservation is made on it. + /// + /// Size of the memory range to reserve. + /// Set to true if the memory range must be executable. + /// The reserved memory range. + private AllocatedRange ReserveOrAllocateRange(ulong size, bool requireExecutable) + { + uint alignment = _is64Bits ? (uint)8 : 4; + return _allocatedRanges.Select(r => r.TryReserveRange(size, alignment)) + .FirstOrDefault(r => r != null) + ?? Allocate(size, requireExecutable).ReserveRange(size, alignment); + } + + /// + /// Stores the given data in the process memory. If needed, memory is allocated to store the data. Returns the + /// reserved range that you can utilize to use the data. + /// + /// Data to store. + /// Set to true if the data is executable code. Defaults to false. + /// The reserved memory range. + public AllocatedRange Store(byte[] data, bool isCode = false) + { + var reservedRange = ReserveOrAllocateRange((ulong)data.Length, isCode); + WriteBytes(reservedRange.Range.Start, data, MemoryProtectionStrategy.Ignore); + return reservedRange; + } + + /// + /// Stores the given data in the specified range of memory. Returns the reserved range that you can utilize to use + /// the data. + /// + /// Data to store. + /// Range of memory to store the data in. + /// The reserved memory range. + public AllocatedRange Store(byte[] data, AllocatedRange range) + { + uint alignment = _is64Bits ? (uint)8 : 4; + var reservedRange = range.ReserveRange((ulong)data.Length, alignment); + WriteBytes(reservedRange.Range.Start, data, MemoryProtectionStrategy.Ignore); + return reservedRange; + } + + /// + /// Stores the given value or structure in the process memory. If needed, memory is allocated to store the data. + /// Returns the reserved range that you can utilize to use the data. + /// + /// Value or structure to store. + /// Type of the value or structure. + /// The reserved memory range. + public AllocatedRange Store(T value) + => Store(value.ToBytes(), false); + + /// + /// Stores the given value or structure in the specified range of memory. Returns the reserved range that you can + /// utilize to use the data. + /// + /// Value or structure to store. + /// Range of memory to store the data in. + /// Type of the value or structure. + /// The reserved memory range. + public AllocatedRange Store(T value, AllocatedRange range) where T: struct + => Store(value.ToBytes(), range); + + #endregion } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs index 308238a..a303f15 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs @@ -29,6 +29,9 @@ public void Write(PointerPath path, T value, MemoryProtectionStrategy? memory /// Thrown when the type of the value is not supported. public void Write(UIntPtr address, T value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { + if (value == null) + throw new ArgumentNullException(nameof(value), "The value to write cannot be null."); + switch (value) { case bool v: WriteBool(address, v, memoryProtectionStrategy); break; @@ -43,7 +46,7 @@ public void Write(UIntPtr address, T value, MemoryProtectionStrategy? memoryP case ulong v: WriteULong(address, v, memoryProtectionStrategy); break; case double v: WriteDouble(address, v, memoryProtectionStrategy); break; case byte[] v: WriteBytes(address, v, memoryProtectionStrategy); break; - default: throw new ArgumentException($"Writing a value of type \"{typeof(T)}\" is not supported."); + default: WriteBytes(address, value.ToBytes(), memoryProtectionStrategy); break; } } diff --git a/src/MindControl/Ranges/AllocatedRange.cs b/src/MindControl/Ranges/AllocatedRange.cs index 20ffe8f..f75a4ce 100644 --- a/src/MindControl/Ranges/AllocatedRange.cs +++ b/src/MindControl/Ranges/AllocatedRange.cs @@ -29,7 +29,7 @@ public class AllocatedRange : IDisposable public AllocatedRange? ParentRange { get; } /// - /// Gets a boolean indicating if the range has been freed and is thus unavailable for reservations. + /// Gets a boolean indicating if the range is in use. If false, the range has been freed and is no longer usable. /// public bool IsReserved => !_hasBeenFreed; @@ -139,7 +139,7 @@ private IEnumerable GetFreeRanges() .Select(r => byteAlignment == null ? r : r.AlignedTo(byteAlignment.Value)) .Where(r => r.HasValue) .OrderBy(r => r!.Value.Start.ToUInt64()) - .FirstOrDefault(r => r?.GetSize() > size); + .FirstOrDefault(r => r?.GetSize() >= size); if (matchingRange == null) return null; diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index 504674c..d0144a1 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -24,6 +24,117 @@ public void AllocateTest() Assert.That(range, Is.Not.Null); Assert.That(range.Range.GetSize(), Is.AtLeast(0x1000)); + Assert.That(range.IsExecutable, Is.False); Assert.That(TestProcessMemory!.AllocatedRanges, Has.Member(range)); } + + /// + /// Tests the method. + /// Performs an allocation for executable code and verifies that the resulting range is executable. + /// + [Test] + public void AllocateExecutableTest() + { + var range = TestProcessMemory!.Allocate(0x1000, true); + Assert.That(range, Is.Not.Null); + Assert.That(range.Range.GetSize(), Is.AtLeast(0x1000)); + Assert.That(range.IsExecutable, Is.True); + Assert.That(TestProcessMemory!.AllocatedRanges, Has.Member(range)); + } + + /// + /// Tests the method with a zero size. + /// This should throw an exception. + /// + [Test] + public void AllocateZeroTest() + => Assert.Throws(() => TestProcessMemory!.Allocate(0, false)); + + /// + /// Tests the method. + /// Stores a byte array in an allocated range and verifies that the value has been stored properly. + /// + [Test] + public void StoreWithRangeTest() + { + var value = new byte[] { 1, 2, 3, 4 }; + var range = TestProcessMemory!.Allocate(0x1000, false); + + var result = TestProcessMemory.Store(value, range); + var read = TestProcessMemory.ReadBytes(result.Range.Start, value.Length); + + Assert.That(result.IsReserved); + // The resulting range should be a range reserved from our original range. + Assert.That(result.ParentRange, Is.EqualTo(range)); + // When we read over the range, we should get the same value we stored. + Assert.That(read, Is.EqualTo(value)); + Assert.That(TestProcessMemory.AllocatedRanges, Has.Count.EqualTo(1)); + } + + /// + /// Tests the method. + /// Stores a byte array without specifying a range and verifies that the value has been stored properly. + /// + [Test] + public void StoreWithoutRangeTest() + { + var value = new byte[] { 1, 2, 3, 4 }; + + var result = TestProcessMemory!.Store(value); + var read = TestProcessMemory.ReadBytes(result.Range.Start, value.Length); + + // The store method should have allocated a new range. + Assert.That(TestProcessMemory.AllocatedRanges, Has.Count.EqualTo(1)); + Assert.That(result.IsReserved); + // When we read over the range, we should get the same value we stored. + Assert.That(read, Is.EqualTo(value)); + } + + /// + /// Tests the method. + /// Stores a small byte array multiple times without specifying a range, and verifies that only one allocation has + /// been made, with multiple reservations on it. + /// + [Test] + public void StoreWithoutRangeWithMultipleSmallValuesTest() + { + var value = new byte[] { 1, 2, 3, 4 }; + + var results = Enumerable.Range(0, 4).Select(_ => TestProcessMemory!.Store(value)).ToList(); + var readBackValues = results.Select(r => TestProcessMemory!.ReadBytes(r.Range.Start, value.Length)); + + // The store method should have allocated only one range that's big enough to accomodate all the values. + Assert.That(TestProcessMemory!.AllocatedRanges, Has.Count.EqualTo(1)); + + // All the results should be reserved ranges from the same parent range. + Assert.That(results, Has.All.Matches(r => r.IsReserved)); + Assert.That(results, Has.All.Matches(r => r.ParentRange == results.First().ParentRange)); + + // When we read over the ranges, we should get the same value we stored. + Assert.That(readBackValues, Is.All.EqualTo(value)); + } + + /// + /// Tests the method. + /// Allocates a chunk of memory, stores a big value that just fits in the range, and then stores the same value + /// another time. + /// Verifies that the second store operation has allocated a new range. + /// + [Test] + public void StoreWithMultipleOverflowingValuesTest() + { + var range = TestProcessMemory!.Allocate(0x1000, false); + var value = new byte[range.Range.GetSize()]; + + TestProcessMemory!.Store(value); + + // So far, we should have only one allocated range. + Assert.That(TestProcessMemory!.AllocatedRanges, Has.Count.EqualTo(1)); + + // Now we store the same value again, which should overflow the range. + TestProcessMemory!.Store(value); + + // We should have two allocated ranges now, because there is no room left in the first range. + Assert.That(TestProcessMemory!.AllocatedRanges, Has.Count.EqualTo(2)); + } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs index d8ca22b..bc5ca4d 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs @@ -48,7 +48,7 @@ private void AssertFinalResults(int index, string expectedValue) } #endregion - + /// /// Write a value in the target app memory, and assert that the output of the app for that specific value (and no /// other value) reflects the change. @@ -78,4 +78,22 @@ public void WriteValueTest(string pointerPathSuffix, object value, int finalResu ProceedToNextStep(); AssertFinalResults(finalResultsIndex, expectedValue); } + + /// A struct with a couple fields, to test writing structs in the target app memory. + public record struct TestStruct(int A, long B); + + /// + /// Write a struct instance in the target app memory, and attempt to read it back. + /// + [Test] + public void WriteStructTest() + { + var structInstance = new TestStruct(123, -456789); + ProceedToNextStep(); + var pointerPath = new PointerPath($"{OuterClassPointer:X}+50"); + TestProcessMemory!.Write(pointerPath, structInstance); + var valueReadBack = TestProcessMemory.Read(pointerPath); + + Assert.That(valueReadBack, Is.EqualTo(structInstance)); + } } \ No newline at end of file diff --git a/test/MindControl.Test/RangeTests/AllocatedRangeTest.cs b/test/MindControl.Test/RangeTests/AllocatedRangeTest.cs index fe0c8cf..9e1d2dd 100644 --- a/test/MindControl.Test/RangeTests/AllocatedRangeTest.cs +++ b/test/MindControl.Test/RangeTests/AllocatedRangeTest.cs @@ -1,4 +1,5 @@ -using MindControl.Test.ProcessMemoryTests; +using System.ComponentModel; +using MindControl.Test.ProcessMemoryTests; using NUnit.Framework; namespace MindControl.Test.RangeTests; @@ -12,5 +13,90 @@ namespace MindControl.Test.RangeTests; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class AllocatedRangeTest : ProcessMemoryTest { + private AllocatedRange _range; + + /// + /// Initializes the test by allocating a range of memory. + /// + [SetUp] + public void SetUp() + { + // Allocate a range of memory for the tests + _range = TestProcessMemory!.Allocate(0x1000, false); + } + + /// + /// Tests the method. + /// Expects the range to be removed from the list of allocated ranges and the memory to be released. + /// + [Test] + public void DisposeTest() + { + var address = _range.Range.Start; + _range.Dispose(); + + // Check that the range is now unusable + Assert.That(_range.IsReserved, Is.False); + Assert.Throws(() => _range.ReserveRange(0x10)); + + // Check that the range has been removed from the list of allocated ranges + Assert.That(TestProcessMemory!.AllocatedRanges, Is.Empty); + + // Check that the memory has been released (we should not be able to write to it) + Assert.Throws(() => TestProcessMemory.Write(address, 0, MemoryProtectionStrategy.Ignore)); + } + + /// + /// Tests the method on a reservation. + /// Expects the range to be removed from the list of the parent allocated range. + /// + [Test] + public void DisposeReservationTest() + { + var reservation = _range.ReserveRange(0x10); + reservation.Dispose(); + Assert.That(reservation.IsReserved, Is.False); + Assert.That(_range.ReservedRanges, Is.Empty); + Assert.That(_range.GetRemainingSpace(), Is.EqualTo(_range.Range.GetSize())); + } + /// + /// Tests the method. + /// Reserve a single 0x10 portion of memory in the range. + /// + [Test] + public void ReserveTest() + { + var reservedRange = _range.ReserveRange(0x10); + + // Check the reserved range + Assert.That(reservedRange, Is.Not.Null); + Assert.That(reservedRange.Range.GetSize(), Is.EqualTo(0x10)); + Assert.That(reservedRange.IsReserved, Is.True); + Assert.That(reservedRange.ParentRange, Is.EqualTo(_range)); + Assert.That(reservedRange.ReservedRanges, Is.Empty); + Assert.That(reservedRange.GetRemainingSpace(), Is.EqualTo(0x10)); + Assert.That(reservedRange.GetTotalReservedSpace(), Is.Zero); + Assert.That(reservedRange.GetLargestReservableSpace()?.GetSize(), Is.EqualTo(0x10)); + + // Check the effect on the parent range + Assert.That(_range.ReservedRanges, Has.Member(reservedRange)); + Assert.That(_range.ReservedRanges, Has.Count.EqualTo(1)); + } + + /// + /// Tests the method, before and after reservations. + /// + [Test] + public void GetRemainingSpaceTest() + { + Assert.That(_range.GetRemainingSpace(), Is.EqualTo(_range.Range.GetSize())); + _range.ReserveRange(0x10); + var b = _range.ReserveRange(0x10); + _range.ReserveRange(0x10); + Assert.That(_range.GetRemainingSpace(), Is.EqualTo(_range.Range.GetSize() - 0x30)); + + b.Dispose(); + Assert.That(_range.GetRemainingSpace(), Is.EqualTo(_range.Range.GetSize() - 0x20)); + } } From 047f4c4ca732424642274befca1d950ded74f8a2 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sun, 5 May 2024 16:45:34 +0200 Subject: [PATCH 05/66] Split allocations and reservations and added unit tests --- .../{Ranges => Addressing}/MemoryRange.cs | 0 .../{ => Addressing}/PointerPath.cs | 0 .../MemoryAllocation.cs} | 180 ++++---- .../Allocation/MemoryReservation.cs | 48 +++ .../InsufficientAllocatedMemoryException.cs | 4 +- .../MindControl.csproj.DotSettings | 2 + .../ProcessMemory/ProcessMemory.Allocation.cs | 73 ++-- .../ProcessMemory/ProcessMemory.cs | 6 +- .../MemoryRangeTest.cs | 2 +- .../{ => AddressingTests}/PointerPathTest.cs | 2 +- .../AllocationTests/MemoryAllocationTest.cs | 386 ++++++++++++++++++ .../AllocationTests/MemoryReservationTest.cs | 42 ++ .../ProcessMemoryAllocationTest.cs | 81 ++-- .../RangeTests/AllocatedRangeTest.cs | 102 ----- 14 files changed, 650 insertions(+), 278 deletions(-) rename src/MindControl/{Ranges => Addressing}/MemoryRange.cs (100%) rename src/MindControl/{ => Addressing}/PointerPath.cs (100%) rename src/MindControl/{Ranges/AllocatedRange.cs => Allocation/MemoryAllocation.cs} (53%) create mode 100644 src/MindControl/Allocation/MemoryReservation.cs rename test/MindControl.Test/{RangeTests => AddressingTests}/MemoryRangeTest.cs (99%) rename test/MindControl.Test/{ => AddressingTests}/PointerPathTest.cs (99%) create mode 100644 test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs create mode 100644 test/MindControl.Test/AllocationTests/MemoryReservationTest.cs delete mode 100644 test/MindControl.Test/RangeTests/AllocatedRangeTest.cs diff --git a/src/MindControl/Ranges/MemoryRange.cs b/src/MindControl/Addressing/MemoryRange.cs similarity index 100% rename from src/MindControl/Ranges/MemoryRange.cs rename to src/MindControl/Addressing/MemoryRange.cs diff --git a/src/MindControl/PointerPath.cs b/src/MindControl/Addressing/PointerPath.cs similarity index 100% rename from src/MindControl/PointerPath.cs rename to src/MindControl/Addressing/PointerPath.cs diff --git a/src/MindControl/Ranges/AllocatedRange.cs b/src/MindControl/Allocation/MemoryAllocation.cs similarity index 53% rename from src/MindControl/Ranges/AllocatedRange.cs rename to src/MindControl/Allocation/MemoryAllocation.cs index f75a4ce..01fd5a7 100644 --- a/src/MindControl/Ranges/AllocatedRange.cs +++ b/src/MindControl/Allocation/MemoryAllocation.cs @@ -1,62 +1,42 @@ namespace MindControl; /// -/// Represents a range of allocated memory in a process. +/// Represents a range of memory that has been allocated in a process. +/// Can be used to safely manage data or code storage using reservations. /// -public class AllocatedRange : IDisposable +public class MemoryAllocation { /// - /// Gets the memory range of this allocated range. + /// Gets the memory range allocated. /// public MemoryRange Range { get; } - private readonly List _reservedRanges = new(); + private readonly List _reservations = new(); /// - /// Gets the reserved ranges within this range. - /// They are owned by this instance. Do not dispose them manually. + /// Gets the reservations managed by this allocation. /// - public IReadOnlyList ReservedRanges => _reservedRanges.AsReadOnly(); + public IReadOnlyList Reservations => _reservations.AsReadOnly(); /// - /// Gets a value indicating if the memory range has executable permissions. + /// Gets a value indicating if the memory range has been allocated with executable permissions. /// public bool IsExecutable { get; } /// - /// Gets the range containing this range, if any. + /// Gets a boolean indicating if this instance has been disposed. If True, the instance is no longer usable. /// - public AllocatedRange? ParentRange { get; } - - /// - /// Gets a boolean indicating if the range is in use. If false, the range has been freed and is no longer usable. - /// - public bool IsReserved => !_hasBeenFreed; + public bool IsDisposed { get; private set; } private readonly ProcessMemory? _parentProcessMemory; - - private bool _hasBeenFreed; - - /// - /// Builds a new instance with a parent range. - /// - /// Memory range of the allocated range. - /// Value indicating if the memory range has executable permissions. - /// Parent range containing this range. - internal AllocatedRange(MemoryRange range, bool isExecutable, AllocatedRange parentRange) - { - Range = range; - IsExecutable = isExecutable; - ParentRange = parentRange; - } /// - /// Builds a new top-level instance with a parent process memory. + /// Builds a new instance. /// - /// Memory range of the allocated range. + /// Memory range allocated. /// Value indicating if the memory range has executable permissions. - /// Parent process memory instance handling this range. - internal AllocatedRange(MemoryRange range, bool isExecutable, ProcessMemory parentProcessMemory) + /// Instance of handling this allocation. + internal MemoryAllocation(MemoryRange range, bool isExecutable, ProcessMemory parentProcessMemory) { Range = range; IsExecutable = isExecutable; @@ -64,28 +44,28 @@ internal AllocatedRange(MemoryRange range, bool isExecutable, ProcessMemory pare } /// - /// Gets the total space reserved in the range, in bytes. + /// Gets the total space reserved in the allocated range, in bytes. /// - /// The total space reserved in the range. + /// The total space reserved in the allocated range, in bytes. public ulong GetTotalReservedSpace() { - if (_hasBeenFreed) - throw new ObjectDisposedException(nameof(AllocatedRange)); + if (IsDisposed) + throw new ObjectDisposedException(nameof(MemoryAllocation)); - return ReservedRanges.Aggregate(0, + return Reservations.Aggregate(0, (current, subRange) => current + subRange?.Range.GetSize() ?? 0); } /// - /// Gets the total space remaining in the range, in bytes. - /// Note that this does not mean the range is contiguous. Space might be fragmented and thus unable to be - /// reserved entirely in a single allocation. + /// Gets the total space available for reservation in the range, in bytes. + /// Note that space might be fragmented and thus unavailable for a single reservation. Use + /// if you want to make sure your data fits in the range. /// - /// The total space remaining in the range. + /// The total space available for reservation in the range. public ulong GetRemainingSpace() { - if (_hasBeenFreed) - throw new ObjectDisposedException(nameof(AllocatedRange)); + if (IsDisposed) + throw new ObjectDisposedException(nameof(MemoryAllocation)); return Range.GetSize() - GetTotalReservedSpace(); } @@ -97,10 +77,12 @@ public ulong GetRemainingSpace() /// The largest contiguous, unreserved space in the range. public MemoryRange? GetLargestReservableSpace() { - if (_hasBeenFreed) - throw new ObjectDisposedException(nameof(AllocatedRange)); + if (IsDisposed) + throw new ObjectDisposedException(nameof(MemoryAllocation)); - return GetFreeRanges().OrderByDescending(r => r.GetSize()).FirstOrDefault(); + return GetFreeRanges().OrderByDescending(r => r.GetSize()) + .Cast() + .FirstOrDefault(); } /// @@ -111,7 +93,7 @@ private IEnumerable GetFreeRanges() // Take the whole range and exclude all reserved ranges from it. // This will split the range into all free ranges. var freeRanges = new List { Range }; - foreach (var reservedRange in _reservedRanges) + foreach (var reservedRange in _reservations) { freeRanges = freeRanges.SelectMany(r => r.Exclude(reservedRange.Range)).ToList(); } @@ -120,21 +102,27 @@ private IEnumerable GetFreeRanges() } /// - /// Gets the first free range that can fit the specified size, with an optional alignment, or null if no range is - /// large enough to fit the requested size with the specified alignment. + /// Gets the first free range that can fit the specified size, with an optional alignment. Returns null if no range + /// is large enough to fit the requested size with the specified alignment. /// Note that this method does not reserve the range. Most of the time, you should use /// instead of this method. /// /// Requested size of the range to find, in bytes. /// Optional byte alignment for the range. When null, values are not aligned. The /// default value is 8, meaning that for example a range of [0x15,0x3C] will be aligned to [0x18,0x38] and thus - /// only accomodate 32 bytes. + /// only accomodate 32 bytes. Alignment means the resulting range might be bigger than the , + /// but will never make it smaller. /// The first free range that can fit the specified size, or null if no range is large enough. public MemoryRange? GetNextRangeFittingSize(ulong size, uint? byteAlignment = 8) { - if (_hasBeenFreed) - throw new ObjectDisposedException(nameof(AllocatedRange)); + if (IsDisposed) + throw new ObjectDisposedException(nameof(MemoryAllocation)); + // Adjust the size to fit the alignment + if (byteAlignment != null && size % byteAlignment.Value > 0) + size += byteAlignment.Value - size % byteAlignment.Value; + + // Get the first range that can fit the size, aligned if necessary var matchingRange = GetFreeRanges() .Select(r => byteAlignment == null ? r : r.AlignedTo(byteAlignment.Value)) .Where(r => r.HasValue) @@ -144,61 +132,63 @@ private IEnumerable GetFreeRanges() if (matchingRange == null) return null; - // Adjust the end address to fit no more than the requested size - return matchingRange.Value with { End = (UIntPtr)(matchingRange.Value.Start.ToUInt64() + size - 1) }; + // Adjust the range to fit the size exactly + return new MemoryRange(matchingRange.Value.Start, (UIntPtr)(matchingRange.Value.Start.ToUInt64() + size - 1)); } /// /// Reserves and returns the next available range with the specified size. - /// Data stored in the returned range will not be overwritten by future reservations until the range is freed. + /// Data stored in the returned range will not be overwritten by future reservations until the reservation is freed. /// This method throws if there is no contiguous memory large enough in the range. See - /// for a non-throwing alternative. + /// for a non-throwing alternative. /// /// Minimal size that the range to get must be able to accomodate, in bytes. /// Optional byte alignment for the range. When null, values are not aligned. The /// default value is 8, meaning that for example a range of [0x15,0x3C] will be aligned to [0x18,0x38] and thus - /// only accomodate 32 bytes. - /// The reserved range. - public AllocatedRange ReserveRange(ulong size, uint? byteAlignment = 8) + /// only accomodate 32 bytes. Alignment means the actual reserved space might be bigger than the + /// , but will never make it smaller. + /// The resulting reservation. + public MemoryReservation ReserveRange(ulong size, uint? byteAlignment = 8) => TryReserveRange(size, byteAlignment) ?? throw new InsufficientAllocatedMemoryException(size, byteAlignment); /// /// Attempts to reserve and return the next available range with the specified size. - /// Data stored in the returned range will not be overwritten by future reservations until the range is freed. + /// Data stored in the returned range will not be overwritten by future reservations until the reservation is freed. /// This method returns null if there is no contiguous memory large enough in the range. See /// if you expect an exception to be thrown on a reservation failure. - /// Note that this method will still throw if the range has been freed. + /// Note that this method will still throw if the range has been disposed. /// /// Minimal size that the range to get must be able to accomodate, in bytes. /// Optional byte alignment for the range. When null, values are not aligned. The /// default value is 8, meaning that for example a range of [0x15,0x3C] will be aligned to [0x18,0x38] and thus - /// only accomodate 32 bytes. - /// The reserved range. - public AllocatedRange? TryReserveRange(ulong size, uint? byteAlignment = 8) + /// only accomodate 32 bytes. Alignment means the actual reserved space might be bigger than the + /// , but will never make it smaller. + /// The resulting reservation if possible, null otherwise. + public MemoryReservation? TryReserveRange(ulong size, uint? byteAlignment = 8) { - if (_hasBeenFreed) - throw new ObjectDisposedException(nameof(AllocatedRange)); + if (IsDisposed) + throw new ObjectDisposedException(nameof(MemoryAllocation)); var range = GetNextRangeFittingSize(size, byteAlignment); if (range == null) return null; - var reservedRange = new AllocatedRange(range.Value, IsExecutable, this); - _reservedRanges.Add(reservedRange); + var reservedRange = new MemoryReservation(range.Value, this); + _reservations.Add(reservedRange); return reservedRange; } /// - /// Makes reserved space matching the specified range available for future reservations. Affected reserved ranges + /// Makes reserved space overlapping the specified range available for future reservations. Affected reservations /// will be disposed, and may be either completely gone, reduced, or split into two ranges. /// Consider disposing reservations instead, unless you want more control over the range to free. /// /// The range of memory to free from reservations in this instance. public void FreeRange(MemoryRange rangeToFree) { - for (int i = _reservedRanges.Count - 1; i >= 0; i--) + for (int i = _reservations.Count - 1; i >= 0; i--) { - var currentRange = _reservedRanges[i]; + var currentRange = _reservations[i]; var resultingRanges = currentRange.Range.Exclude(rangeToFree).ToArray(); // Reservation is completely gone @@ -208,50 +198,54 @@ public void FreeRange(MemoryRange rangeToFree) // Reservation is reduced else if (resultingRanges.Length == 1 && resultingRanges[0] != currentRange.Range) { + _reservations[i] = new MemoryReservation(resultingRanges[0], this); currentRange.Dispose(); - _reservedRanges[i] = new AllocatedRange(resultingRanges[0], IsExecutable, this); } // Reservation is split into two ranges else if (resultingRanges.Length == 2) { + _reservations[i] = new MemoryReservation(resultingRanges[0], this); + _reservations.Insert(i + 1, new MemoryReservation(resultingRanges[1], this)); currentRange.Dispose(); - _reservedRanges[i] = new AllocatedRange(resultingRanges[0], IsExecutable, this); - _reservedRanges.Insert(i + 1, new AllocatedRange(resultingRanges[1], IsExecutable, this)); } } } /// - /// Removes all reserved ranges from the range, meaning the whole range will be available for reservations. - /// After using this method, data stored in this range may be overwritten by future reservations. + /// Frees up the given reservation. /// - public void Clear() + /// Reservation to free up. + /// This method is internal, as it is intended to be called by the itself + /// when disposing. + internal void FreeReservation(MemoryReservation reservation) => _reservations.Remove(reservation); + + /// + /// Removes all reservations from the range, meaning the whole allocated range will be available for reservations. + /// + public void ClearReservations() { - if (_hasBeenFreed) + if (IsDisposed) return; - foreach (var subRange in _reservedRanges) - subRange.Dispose(); - - _reservedRanges.Clear(); + for (int i = _reservations.Count - 1; i >= 0; i--) + _reservations[i].Dispose(); + + _reservations.Clear(); } /// - /// Releases all reservations used by the , and makes this instance unusable. - /// If this instance is a top-level range, the process memory will be freed as well. + /// Releases all reservations, and frees up the allocated space. After this method is called, this instance will be + /// made unusable, and you may no longer be able to read or write memory in this range. /// public void Dispose() { - if (_hasBeenFreed) + if (IsDisposed) return; - _hasBeenFreed = true; - Clear(); + IsDisposed = true; + ClearReservations(); - if (ParentRange != null) - ParentRange._reservedRanges.Remove(this); - else - _parentProcessMemory?.Free(this); + _parentProcessMemory?.Free(this); } } \ No newline at end of file diff --git a/src/MindControl/Allocation/MemoryReservation.cs b/src/MindControl/Allocation/MemoryReservation.cs new file mode 100644 index 0000000..c595483 --- /dev/null +++ b/src/MindControl/Allocation/MemoryReservation.cs @@ -0,0 +1,48 @@ +namespace MindControl; + +/// +/// Represents a reservation of a range of memory within a . +/// Reservations within an allocation cannot overlap and thus can be used to safely manage data or code storage over +/// a process. +/// Disposing a reservation will free the memory range for other uses. +/// +public class MemoryReservation +{ + /// + /// Gets the memory range of this reservation. + /// + public MemoryRange Range { get; } + + /// + /// Gets the allocation that handles this reservation. + /// + public MemoryAllocation ParentAllocation { get; } + + /// + /// Gets a boolean indicating if the reservation has been disposed. + /// + public bool IsDisposed { get; private set; } + + /// + /// Builds a new instance. + /// + /// Memory range of the reservation. + /// Allocation that handles this reservation. + internal MemoryReservation(MemoryRange range, MemoryAllocation parentAllocation) + { + Range = range; + ParentAllocation = parentAllocation; + } + + /// + /// Releases this reservation, allowing the parent to reuse the space for other data. + /// + public void Dispose() + { + if (IsDisposed) + return; + + IsDisposed = true; + ParentAllocation.FreeReservation(this); + } +} \ No newline at end of file diff --git a/src/MindControl/InsufficientAllocatedMemoryException.cs b/src/MindControl/InsufficientAllocatedMemoryException.cs index 9d0aa93..2fa33cc 100644 --- a/src/MindControl/InsufficientAllocatedMemoryException.cs +++ b/src/MindControl/InsufficientAllocatedMemoryException.cs @@ -1,7 +1,7 @@ namespace MindControl; /// -/// Exception thrown by an when trying to reserve a memory range with a size exceeding +/// Exception thrown by a when trying to reserve a memory range with a size exceeding /// the largest contiguous unreserved space. /// public class InsufficientAllocatedMemoryException : Exception @@ -22,7 +22,7 @@ public class InsufficientAllocatedMemoryException : Exception /// Requested size for the reservation attempt. /// Requested byte alignment for the reservation attempt. public InsufficientAllocatedMemoryException(ulong requestedSize, uint? requestedAlignment) - : base($"The requested size of {requestedSize} bytes with {(requestedAlignment == null ? "no alignment" : $"an alignment of {requestedAlignment.Value}")} bytes exceeds the largest contiguous unreserved space of the {nameof(AllocatedRange)} instance. Consider allocating more space, reserving multiple smaller blocks, or letting the {nameof(ProcessMemory)} instance handle allocations by using addressless Write method signatures. Read the \"Allocating memory\" section in the documentation for more information.") + : base($"The requested size of {requestedSize} bytes with {(requestedAlignment == null ? "no alignment" : $"an alignment of {requestedAlignment.Value}")} bytes exceeds the largest contiguous unreserved space of the {nameof(MemoryAllocation)} instance. Consider allocating more space, reserving multiple smaller blocks, or letting the {nameof(ProcessMemory)} instance handle allocations by using addressless Write method signatures. Read the \"Allocating memory\" section in the documentation for more information.") { RequestedSize = requestedSize; RequestedAlignment = requestedAlignment; diff --git a/src/MindControl/MindControl.csproj.DotSettings b/src/MindControl/MindControl.csproj.DotSettings index 9922787..525d991 100644 --- a/src/MindControl/MindControl.csproj.DotSettings +++ b/src/MindControl/MindControl.csproj.DotSettings @@ -1,3 +1,5 @@  + True + True True True \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index 275f21e..2dd0c3a 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -6,13 +6,13 @@ namespace MindControl; // This partial class implements methods related to memory allocation. public partial class ProcessMemory { - private readonly List _allocatedRanges = new(); + private readonly List _allocations = new(); /// - /// Gets the ranges allocated in this process. - /// Dispose a range to free the memory and remove it from this list. + /// Gets the ranges that have been allocated for this process. Dispose a range to free the memory and remove it + /// from this list. /// - public IReadOnlyList AllocatedRanges => _allocatedRanges.AsReadOnly(); + public IReadOnlyList Allocations => _allocations.AsReadOnly(); /// /// Attempts to allocate a memory range of the given size within the process. @@ -21,7 +21,7 @@ public partial class ProcessMemory /// Determines if the memory range can be used to store executable code. /// Specify this parameter to limit the allocation to a specific range of memory. /// The allocated memory range. - public AllocatedRange Allocate(ulong size, bool forExecutableCode, MemoryRange? limitRange = null) + public MemoryAllocation Allocate(ulong size, bool forExecutableCode, MemoryRange? limitRange = null) { if (size == 0) throw new ArgumentException("The size of the memory range to allocate must be greater than zero.", @@ -33,24 +33,24 @@ public AllocatedRange Allocate(ulong size, bool forExecutableCode, MemoryRange? throw new InvalidOperationException("No suitable free memory range was found."); // Add the range to the list of allocated ranges and return it - var allocatedRange = new AllocatedRange(range.Value, forExecutableCode, this); - _allocatedRanges.Add(allocatedRange); + var allocatedRange = new MemoryAllocation(range.Value, forExecutableCode, this); + _allocations.Add(allocatedRange); return allocatedRange; } /// - /// Frees the memory allocated for the given range. + /// Releases a range of allocated memory. /// - /// Range to free. - /// This method is internal because it is designed to be called by . - /// Users would release memory by disposing ranges. - internal void Free(AllocatedRange range) + /// Allocation to free. + /// This method is internal because it is designed to be called by . + /// Users would release memory by disposing instances. + internal void Free(MemoryAllocation allocation) { - if (!_allocatedRanges.Contains(range)) + if (!_allocations.Contains(allocation)) return; - _allocatedRanges.Remove(range); - _osService.ReleaseMemory(_processHandle, range.Range.Start); + _allocations.Remove(allocation); + _osService.ReleaseMemory(_processHandle, allocation.Range.Start); } /// @@ -133,16 +133,16 @@ internal void Free(AllocatedRange range) #region Store /// - /// Reserves a range of memory of the given size. If no suitable range is found, a new range is allocated, and - /// a reservation is made on it. + /// Reserves a range of memory of the given size. If no suitable range is found within the current allocations, + /// a new range is allocated, and a reservation is made on it. /// /// Size of the memory range to reserve. /// Set to true if the memory range must be executable. - /// The reserved memory range. - private AllocatedRange ReserveOrAllocateRange(ulong size, bool requireExecutable) + /// The resulting reservation. + private MemoryReservation FindOrMakeReservation(ulong size, bool requireExecutable) { uint alignment = _is64Bits ? (uint)8 : 4; - return _allocatedRanges.Select(r => r.TryReserveRange(size, alignment)) + return _allocations.Select(r => r.TryReserveRange(size, alignment)) .FirstOrDefault(r => r != null) ?? Allocate(size, requireExecutable).ReserveRange(size, alignment); } @@ -154,48 +154,47 @@ private AllocatedRange ReserveOrAllocateRange(ulong size, bool requireExecutable /// Data to store. /// Set to true if the data is executable code. Defaults to false. /// The reserved memory range. - public AllocatedRange Store(byte[] data, bool isCode = false) + public MemoryReservation Store(byte[] data, bool isCode = false) { - var reservedRange = ReserveOrAllocateRange((ulong)data.Length, isCode); + var reservedRange = FindOrMakeReservation((ulong)data.Length, isCode); WriteBytes(reservedRange.Range.Start, data, MemoryProtectionStrategy.Ignore); return reservedRange; } /// - /// Stores the given data in the specified range of memory. Returns the reserved range that you can utilize to use - /// the data. + /// Stores the given data in the specified allocated range. Returns the reservation that holds the data. /// /// Data to store. - /// Range of memory to store the data in. - /// The reserved memory range. - public AllocatedRange Store(byte[] data, AllocatedRange range) + /// Allocated memory to store the data. + /// The reservation holding the data. + public MemoryReservation Store(byte[] data, MemoryAllocation allocation) { uint alignment = _is64Bits ? (uint)8 : 4; - var reservedRange = range.ReserveRange((ulong)data.Length, alignment); + var reservedRange = allocation.ReserveRange((ulong)data.Length, alignment); WriteBytes(reservedRange.Range.Start, data, MemoryProtectionStrategy.Ignore); return reservedRange; } /// /// Stores the given value or structure in the process memory. If needed, memory is allocated to store the data. - /// Returns the reserved range that you can utilize to use the data. + /// Returns the reservation that holds the data. /// /// Value or structure to store. /// Type of the value or structure. - /// The reserved memory range. - public AllocatedRange Store(T value) + /// The reservation holding the data. + public MemoryReservation Store(T value) => Store(value.ToBytes(), false); /// - /// Stores the given value or structure in the specified range of memory. Returns the reserved range that you can - /// utilize to use the data. + /// Stores the given value or structure in the specified range of memory. Returns the reservation that holds the + /// data. /// /// Value or structure to store. - /// Range of memory to store the data in. + /// Range of memory to store the data in. /// Type of the value or structure. - /// The reserved memory range. - public AllocatedRange Store(T value, AllocatedRange range) where T: struct - => Store(value.ToBytes(), range); + /// The reservation holding the data. + public MemoryReservation Store(T value, MemoryAllocation allocation) where T: struct + => Store(value.ToBytes(), allocation); #endregion } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.cs b/src/MindControl/ProcessMemory/ProcessMemory.cs index 17d7782..98fba12 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.cs @@ -174,16 +174,16 @@ private void Detach() if (IsAttached) { IsAttached = false; - for (int i = _allocatedRanges.Count - 1; i >= 0; i--) + for (int i = _allocations.Count - 1; i >= 0; i--) { try { - _allocatedRanges[i].Dispose(); + _allocations[i].Dispose(); } catch (Exception) { // Just skip. We probably lost the process and thus cannot do anything with it anymore. - _allocatedRanges.RemoveAt(i); + _allocations.RemoveAt(i); } } diff --git a/test/MindControl.Test/RangeTests/MemoryRangeTest.cs b/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs similarity index 99% rename from test/MindControl.Test/RangeTests/MemoryRangeTest.cs rename to test/MindControl.Test/AddressingTests/MemoryRangeTest.cs index b5a4b70..54a8e6a 100644 --- a/test/MindControl.Test/RangeTests/MemoryRangeTest.cs +++ b/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs @@ -1,6 +1,6 @@ using NUnit.Framework; -namespace MindControl.Test.RangeTests; +namespace MindControl.Test.AddressingTests; /// /// Tests the class. diff --git a/test/MindControl.Test/PointerPathTest.cs b/test/MindControl.Test/AddressingTests/PointerPathTest.cs similarity index 99% rename from test/MindControl.Test/PointerPathTest.cs rename to test/MindControl.Test/AddressingTests/PointerPathTest.cs index ac83b99..70857d1 100644 --- a/test/MindControl.Test/PointerPathTest.cs +++ b/test/MindControl.Test/AddressingTests/PointerPathTest.cs @@ -1,7 +1,7 @@ using System.Numerics; using NUnit.Framework; -namespace MindControl.Test; +namespace MindControl.Test.AddressingTests; /// /// Tests the class. diff --git a/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs b/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs new file mode 100644 index 0000000..833db2d --- /dev/null +++ b/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs @@ -0,0 +1,386 @@ +using System.ComponentModel; +using MindControl.Test.ProcessMemoryTests; +using NUnit.Framework; + +namespace MindControl.Test.AllocationTests; + +/// +/// Tests the class. +/// Because this class is strongly bound to a ProcessMemory, we have to use the +/// method to create instances, so the tests below will use an actual instance of and +/// depend on that method. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class MemoryAllocationTest : ProcessMemoryTest +{ + private MemoryAllocation _allocation; + + /// + /// Initializes the test by allocating a range of memory. + /// + [SetUp] + public void SetUp() + { + // Allocate a range of memory for the tests + _allocation = TestProcessMemory!.Allocate(0x1000, false); + } + + /// + /// Tests the method. + /// Expects the allocation to be removed from the list of managed allocations, and the memory to be released. + /// + [Test] + public void DisposeTest() + { + var address = _allocation.Range.Start; + _allocation.Dispose(); + + // Check that the allocation instance is now disposed and unusable + Assert.That(_allocation.IsDisposed, Is.True); + Assert.Throws(() => _allocation.ReserveRange(0x10)); + + // Check that the allocation has been removed from the list + Assert.That(TestProcessMemory!.Allocations, Is.Empty); + + // Check that the memory has been released (we should not be able to write to it) + Assert.Throws(() => TestProcessMemory.Write(address, 0, MemoryProtectionStrategy.Ignore)); + } + + /// + /// Tests the method. + /// Reserve a single 0x10 portion of memory in the range and check the reservation and allocation properties. + /// + [Test] + public void ReserveRangeTest() + { + var reservedRange = _allocation.ReserveRange(0x10); + + // Check the reserved range + Assert.That(reservedRange, Is.Not.Null); + Assert.That(reservedRange.Range.GetSize(), Is.EqualTo(0x10)); + Assert.That(reservedRange.IsDisposed, Is.False); + + // Check that the reservation is added to the list in the allocation instance + Assert.That(_allocation.Reservations, Has.Member(reservedRange)); + Assert.That(_allocation.Reservations, Has.Count.EqualTo(1)); + } + + /// + /// Tests the method. + /// Reserve the whole allocation range in one time. + /// + [Test] + public void ReserveRangeWithFullRangeTest() + { + // Just test that it doesn't throw. + _allocation.ReserveRange(_allocation.Range.GetSize()); + } + + /// + /// Tests the method. + /// Attempt to reserve 0 bytes. This should throw. + /// + [Test] + public void ReserveRangeWithZeroSizeTest() + => Assert.Throws(() => _allocation.ReserveRange(0)); + + /// + /// Tests the method. + /// Attempt to reserve 1 byte more than the size of the full reservation range. This should throw. + /// + [Test] + public void ReserveRangeWithRangeLargerThanAllocationTest() + => Assert.Throws(() => _allocation.ReserveRange(0x1001)); + + /// + /// Tests the method. + /// Reserve 0x100 bytes, and then attempt to reserve 0x1000 bytes (full allocation range). + /// The second reservation should throw. + /// + [Test] + public void ReserveRangeWithMultipleReservationsTooLargeToFitTest() + { + _allocation.ReserveRange(0x100); + Assert.Throws(() => _allocation.ReserveRange(0x1000)); + } + + /// + /// Tests the method. + /// Reserve 5 bytes, two times in a row, with an alignment of 4. + /// The resulting reservations should be [0,7] and [8,15], relative to the start of the allocated range. + /// + [Test] + public void ReserveRangeWithRealignmentTest() + { + var a = _allocation.ReserveRange(5, byteAlignment: 4); + var b = _allocation.ReserveRange(5, byteAlignment: 4); + + Assert.That(a.Range, Is.EqualTo(new MemoryRange(_allocation.Range.Start, _allocation.Range.Start + 7))); + Assert.That(b.Range, Is.EqualTo(new MemoryRange(_allocation.Range.Start + 8, _allocation.Range.Start + 15))); + } + + /// + /// Tests the method. + /// Reserve 5 bytes, two times in a row, with no alignment. + /// The resulting reservations should be [0,4] and [5,9], relative to the start of the allocated range. + /// + [Test] + public void ReserveRangeWithNoAlignmentTest() + { + var a = _allocation.ReserveRange(5, byteAlignment: null); + var b = _allocation.ReserveRange(5, byteAlignment: null); + + Assert.That(a.Range, Is.EqualTo(new MemoryRange(_allocation.Range.Start, _allocation.Range.Start + 4))); + Assert.That(b.Range, Is.EqualTo(new MemoryRange(_allocation.Range.Start + 5, _allocation.Range.Start + 9))); + } + + /// + /// Tests the method, before and after reservations. + /// + [Test] + public void GetRemainingSpaceTest() + { + Assert.That(_allocation.GetRemainingSpace(), Is.EqualTo(_allocation.Range.GetSize())); + + // Make 3 0x10 allocations and check that the remaining space is total space - 0x30 + _allocation.ReserveRange(0x10); + var b = _allocation.ReserveRange(0x10); + _allocation.ReserveRange(0x10); + Assert.That(_allocation.GetRemainingSpace(), Is.EqualTo(_allocation.Range.GetSize() - 0x30)); + + // Dispose the 2nd reservation and check that the remaining space is total space - 0x20 + b.Dispose(); + Assert.That(_allocation.GetRemainingSpace(), Is.EqualTo(_allocation.Range.GetSize() - 0x20)); + } + + /// + /// Tests the method, before and after reservations. + /// + [Test] + public void GetTotalReservedSpaceTest() + { + Assert.That(_allocation.GetTotalReservedSpace(), Is.Zero); + + // Make 3 0x10 allocations and check that the reserved space is 0x30 + _allocation.ReserveRange(0x10); + var b = _allocation.ReserveRange(0x10); + _allocation.ReserveRange(0x10); + Assert.That(_allocation.GetTotalReservedSpace(), Is.EqualTo(0x30)); + + // Dispose the 2nd reservation and check that the reserved space is 0x20 + b.Dispose(); + Assert.That(_allocation.GetTotalReservedSpace(), Is.EqualTo(0x20)); + } + + /// + /// Tests the method, before and after reservations. + /// + [Test] + public void GetLargestReservableSpaceTest() + { + Assert.That(_allocation.GetLargestReservableSpace(), Is.EqualTo(_allocation.Range)); + + // Make 3 0x500 allocations. The largest unreserved space should now be the last 0x100 bytes of the range. + _allocation.ReserveRange(0x500); + var b = _allocation.ReserveRange(0x500); + _allocation.ReserveRange(0x500); + var expectedRange = new MemoryRange(_allocation.Range.End - 0xFF, _allocation.Range.End); + Assert.That(_allocation.GetLargestReservableSpace(), Is.EqualTo(expectedRange)); + + // Dispose the 2nd reservation. Its range should now be the largest unreserved space. + b.Dispose(); + Assert.That(_allocation.GetLargestReservableSpace(), Is.EqualTo(b.Range)); + } + + /// + /// Tests the method, with no space remaining. + /// + [Test] + public void GetLargestReservableSpaceWithNoSpaceRemainingTest() + { + _allocation.ReserveRange(_allocation.Range.GetSize()); + Assert.That(_allocation.GetLargestReservableSpace(), Is.Null); + } + + /// + /// Tests the method, before and after reservations. + /// + [Test] + public void GetNextRangeFittingSizeTest() + { + Assert.That(_allocation.GetNextRangeFittingSize(0x10), + Is.EqualTo(new MemoryRange(_allocation.Range.Start, _allocation.Range.Start + 0xF))); + + // Make 3 0x10 allocations. The next available 0x10 space should be right after these three. + _allocation.ReserveRange(0x10); + var b = _allocation.ReserveRange(0x10); + _allocation.ReserveRange(0x10); + Assert.That(_allocation.GetNextRangeFittingSize(0x10), + Is.EqualTo(new MemoryRange(_allocation.Range.Start + 0x30, _allocation.Range.Start + 0x3F))); + + // Dispose the 2nd reservation. The next available 0x10 space should now be the original 2nd reservation range. + b.Dispose(); + Assert.That(_allocation.GetNextRangeFittingSize(0x10), Is.EqualTo(b.Range)); + } + + /// + /// Tests the method, with the size of the whole allocated + /// range. + /// The resulting range should be the same as the allocation range. + /// + [Test] + public void GetNextRangeFittingSizeWithFullAllocationRangeTest() + { + Assert.That(_allocation.GetNextRangeFittingSize(_allocation.Range.GetSize()), Is.EqualTo(_allocation.Range)); + } + + /// + /// Tests the method. + /// Reserves the full allocated range, and then attempt to get the next range fitting 1 byte. + /// The result should be null. + /// + [Test] + public void GetNextRangeFittingSizeWithNoSpaceRemainingTest() + { + _allocation.ReserveRange(_allocation.Range.GetSize()); + Assert.That(_allocation.GetNextRangeFittingSize(1), Is.Null); + } + + /// + /// Tests the method. + /// Call the method with a size of 5 bytes, and an alignment of 4. + /// The resulting range should be [0,7], relative to the start of the allocated range. + /// + [Test] + public void GetNextRangeFittingSizeWithRealignmentTest() + { + Assert.That(_allocation.GetNextRangeFittingSize(5, byteAlignment: 4), + Is.EqualTo(new MemoryRange(_allocation.Range.Start, _allocation.Range.Start + 7))); + } + + /// + /// Tests the method. + /// Call the method with a size of 5 bytes, and no alignment. + /// The resulting range should be [0,4], relative to the start of the allocated range. + /// + [Test] + public void GetNextRangeFittingSizeWithNoAlignmentTest() + { + Assert.That(_allocation.GetNextRangeFittingSize(5, byteAlignment: null), + Is.EqualTo(new MemoryRange(_allocation.Range.Start, _allocation.Range.Start + 4))); + } + + /// + /// Tests the method. + /// Reserve a range of 0x10 bytes, and then free the range of that reservation. + /// After this operation, there should be no reservations in the allocation, and the reservation should be disposed. + /// + [Test] + public void FreeRangeWithFullReservationRangeTest() + { + var reservation = _allocation.ReserveRange(0x10); + _allocation.FreeRange(reservation.Range); + + Assert.That(reservation.IsDisposed, Is.True); + Assert.That(_allocation.Reservations, Is.Empty); + } + + /// + /// Tests the method. + /// Reserve a range of 0x10 bytes, and then free the range [-4,4], relative to the start of the allocated range. + /// After this operation, the reservation should be disposed, and a new reservation with a range of [5,F] should + /// be present in the allocation. + /// + [Test] + public void FreeRangeWithRangeOverlappingStartOfExistingReservationTest() + { + var reservation = _allocation.ReserveRange(0x10); + _allocation.FreeRange(new MemoryRange(reservation.Range.Start - 4, reservation.Range.Start + 4)); + + Assert.That(reservation.IsDisposed, Is.True); + Assert.That(_allocation.Reservations, Has.Count.EqualTo(1)); + Assert.That(_allocation.Reservations[0].Range, + Is.EqualTo(new MemoryRange(reservation.Range.Start + 5, reservation.Range.End))); + } + + /// + /// Tests the method. + /// Reserve a range of 0x10 bytes, and then free the range [4,F], relative to the start of the allocated range. + /// After this operation, the reservation should be disposed, and a new reservation with a range of [0,3] should + /// be present in the allocation. + /// + [Test] + public void FreeRangeWithRangeOverlappingEndOfExistingReservationTest() + { + var reservation = _allocation.ReserveRange(0x10); + _allocation.FreeRange(new MemoryRange(reservation.Range.Start + 4, reservation.Range.Start + 0xF)); + + Assert.That(reservation.IsDisposed, Is.True); + Assert.That(_allocation.Reservations, Has.Count.EqualTo(1)); + Assert.That(_allocation.Reservations[0].Range, + Is.EqualTo(new MemoryRange(reservation.Range.Start, reservation.Range.Start + 3))); + } + + /// + /// Tests the method. + /// Reserve a range of 0x10 bytes, and then free the range [4,6], relative to the start of the allocated range. + /// After this operation, the reservation should be disposed, and two new reservations, with ranges of [0,3] and + /// [7,F], should be present in the allocation. + /// + [Test] + public void FreeRangeWithRangeInsideExistingReservationTest() + { + var reservation = _allocation.ReserveRange(0x10); + _allocation.FreeRange(new MemoryRange(reservation.Range.Start + 4, reservation.Range.Start + 6)); + + Assert.That(reservation.IsDisposed, Is.True); + Assert.That(_allocation.Reservations, Has.Count.EqualTo(2)); + Assert.That(_allocation.Reservations[0].Range, + Is.EqualTo(new MemoryRange(reservation.Range.Start, reservation.Range.Start + 3))); + Assert.That(_allocation.Reservations[1].Range, + Is.EqualTo(new MemoryRange(reservation.Range.Start + 7, reservation.Range.End))); + } + + /// + /// Tests the method. + /// Reserve 3 ranges of 0x10 bytes, and then free the range [0x8,0x1F], relative to the start of the allocated + /// range. + /// After this operation, the 2 first reservations should be disposed, and the allocation should hold 2 reservations + /// with the original third reservation, and a new one with a range of [0,7]. + /// + [Test] + public void FreeRangeWithRangeOverlappingMultipleExistingReservationsTest() + { + var a = _allocation.ReserveRange(0x10); + var b = _allocation.ReserveRange(0x10); + var c = _allocation.ReserveRange(0x10); + _allocation.FreeRange(new MemoryRange(a.Range.Start + 8, b.Range.End)); + + Assert.That(a.IsDisposed, Is.True); + Assert.That(b.IsDisposed, Is.True); + Assert.That(c.IsDisposed, Is.False); + Assert.That(_allocation.Reservations, Has.Count.EqualTo(2)); + Assert.That(_allocation.Reservations.Select(r => r.Range), + Has.Member(new MemoryRange(a.Range.Start, a.Range.Start + 7))); + Assert.That(_allocation.Reservations, Has.Member(c)); + } + + /// + /// Tests the method. + /// Reserve 3 ranges, and then clear the reservations. The reservations should be disposed, and the allocation + /// should have no reservations. + /// + [Test] + public void ClearReservationsTest() + { + var a = _allocation.ReserveRange(0x10); + var b = _allocation.ReserveRange(0x10); + var c = _allocation.ReserveRange(0x10); + _allocation.ClearReservations(); + + Assert.That(a.IsDisposed, Is.True); + Assert.That(b.IsDisposed, Is.True); + Assert.That(c.IsDisposed, Is.True); + Assert.That(_allocation.Reservations, Is.Empty); + } +} diff --git a/test/MindControl.Test/AllocationTests/MemoryReservationTest.cs b/test/MindControl.Test/AllocationTests/MemoryReservationTest.cs new file mode 100644 index 0000000..391afa5 --- /dev/null +++ b/test/MindControl.Test/AllocationTests/MemoryReservationTest.cs @@ -0,0 +1,42 @@ +using MindControl.Test.ProcessMemoryTests; +using NUnit.Framework; + +namespace MindControl.Test.AllocationTests; + +/// +/// Tests the class. +/// Because this class is strongly bound to , which is itself bound to ProcessMemory, +/// we have to use the method to create instances, so the tests below will use an +/// actual instance of and depend on that method. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class MemoryReservationTest : ProcessMemoryTest +{ + private MemoryAllocation _allocation; + private MemoryReservation _reservation; + + /// + /// Initializes the test by allocating a range of memory and making a reservation. + /// + [SetUp] + public void SetUp() + { + // Allocate a range of memory for the tests + _allocation = TestProcessMemory!.Allocate(0x1000, false); + + // Reserve a portion of the memory + _reservation = _allocation.ReserveRange(0x10); + } + + /// + /// Tests the method. + /// Expects the instance to be disposed and unusable, and to be removed from the list of the parent allocation. + /// + [Test] + public void DisposeReservationTest() + { + _reservation.Dispose(); + Assert.That(_reservation.IsDisposed, Is.True); + Assert.That(_allocation.Reservations, Is.Empty); + } +} \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index d0144a1..6e55fb8 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -10,36 +10,39 @@ public class ProcessMemoryAllocationTest : ProcessMemoryTest { /// /// Tests the method. - /// Performs a simple allocation and verifies that it is writable and the range is in the list of allocated ranges. + /// Performs a simple allocation and verifies that it is writable and that it is added to the list of managed + /// allocations. /// [Test] public void AllocateTest() { - var range = TestProcessMemory!.Allocate(0x1000, false); + var allocation = TestProcessMemory!.Allocate(0x1000, false); // To check that the memory is writable, we will write a byte array to the allocated range. - // We will use the WriteBytes method rather than Store, because Store is built on top of Allocate and we only + // We will use the WriteBytes method rather than Store, because Store is built on top of Allocate, and we only // want to test the allocation here. - TestProcessMemory.WriteBytes(range.Range.Start, new byte[0x1000]); + TestProcessMemory.WriteBytes(allocation.Range.Start, new byte[0x1000]); - Assert.That(range, Is.Not.Null); - Assert.That(range.Range.GetSize(), Is.AtLeast(0x1000)); - Assert.That(range.IsExecutable, Is.False); - Assert.That(TestProcessMemory!.AllocatedRanges, Has.Member(range)); + Assert.That(allocation, Is.Not.Null); + Assert.That(allocation.IsDisposed, Is.False); + Assert.That(allocation.Range.GetSize(), Is.AtLeast(0x1000)); + Assert.That(allocation.IsExecutable, Is.False); + Assert.That(TestProcessMemory!.Allocations, Has.Member(allocation)); } /// /// Tests the method. - /// Performs an allocation for executable code and verifies that the resulting range is executable. + /// Performs an allocation for executable code and verifies that the resulting allocation is executable. /// [Test] public void AllocateExecutableTest() { - var range = TestProcessMemory!.Allocate(0x1000, true); - Assert.That(range, Is.Not.Null); - Assert.That(range.Range.GetSize(), Is.AtLeast(0x1000)); - Assert.That(range.IsExecutable, Is.True); - Assert.That(TestProcessMemory!.AllocatedRanges, Has.Member(range)); + var allocation = TestProcessMemory!.Allocate(0x1000, true); + Assert.That(allocation, Is.Not.Null); + Assert.That(allocation.IsDisposed, Is.False); + Assert.That(allocation.Range.GetSize(), Is.AtLeast(0x1000)); + Assert.That(allocation.IsExecutable, Is.True); + Assert.That(TestProcessMemory!.Allocations, Has.Member(allocation)); } /// @@ -51,24 +54,24 @@ public void AllocateZeroTest() => Assert.Throws(() => TestProcessMemory!.Allocate(0, false)); /// - /// Tests the method. + /// Tests the method. /// Stores a byte array in an allocated range and verifies that the value has been stored properly. /// [Test] public void StoreWithRangeTest() { var value = new byte[] { 1, 2, 3, 4 }; - var range = TestProcessMemory!.Allocate(0x1000, false); + var allocation = TestProcessMemory!.Allocate(0x1000, false); - var result = TestProcessMemory.Store(value, range); - var read = TestProcessMemory.ReadBytes(result.Range.Start, value.Length); + var reservation = TestProcessMemory.Store(value, allocation); + var read = TestProcessMemory.ReadBytes(reservation.Range.Start, value.Length); - Assert.That(result.IsReserved); + Assert.That(reservation.IsDisposed, Is.False); // The resulting range should be a range reserved from our original range. - Assert.That(result.ParentRange, Is.EqualTo(range)); + Assert.That(reservation.ParentAllocation, Is.EqualTo(allocation)); // When we read over the range, we should get the same value we stored. Assert.That(read, Is.EqualTo(value)); - Assert.That(TestProcessMemory.AllocatedRanges, Has.Count.EqualTo(1)); + Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); } /// @@ -76,39 +79,39 @@ public void StoreWithRangeTest() /// Stores a byte array without specifying a range and verifies that the value has been stored properly. /// [Test] - public void StoreWithoutRangeTest() + public void StoreWithoutPreAllocationTest() { var value = new byte[] { 1, 2, 3, 4 }; - var result = TestProcessMemory!.Store(value); - var read = TestProcessMemory.ReadBytes(result.Range.Start, value.Length); + var reservation = TestProcessMemory!.Store(value); + var read = TestProcessMemory.ReadBytes(reservation.Range.Start, value.Length); // The store method should have allocated a new range. - Assert.That(TestProcessMemory.AllocatedRanges, Has.Count.EqualTo(1)); - Assert.That(result.IsReserved); + Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); + Assert.That(reservation.IsDisposed, Is.False); // When we read over the range, we should get the same value we stored. Assert.That(read, Is.EqualTo(value)); } /// /// Tests the method. - /// Stores a small byte array multiple times without specifying a range, and verifies that only one allocation has - /// been made, with multiple reservations on it. + /// Stores a small byte array multiple times without specifying an allocation, and verifies that only one + /// allocation has been made, with multiple reservations on it. /// [Test] - public void StoreWithoutRangeWithMultipleSmallValuesTest() + public void StoreWithoutPreAllocationWithMultipleSmallValuesTest() { var value = new byte[] { 1, 2, 3, 4 }; - var results = Enumerable.Range(0, 4).Select(_ => TestProcessMemory!.Store(value)).ToList(); - var readBackValues = results.Select(r => TestProcessMemory!.ReadBytes(r.Range.Start, value.Length)); + var reservations = Enumerable.Range(0, 4).Select(_ => TestProcessMemory!.Store(value)).ToList(); + var readBackValues = reservations.Select(r => TestProcessMemory!.ReadBytes(r.Range.Start, value.Length)); // The store method should have allocated only one range that's big enough to accomodate all the values. - Assert.That(TestProcessMemory!.AllocatedRanges, Has.Count.EqualTo(1)); + Assert.That(TestProcessMemory!.Allocations, Has.Count.EqualTo(1)); + var allocation = TestProcessMemory.Allocations.Single(); - // All the results should be reserved ranges from the same parent range. - Assert.That(results, Has.All.Matches(r => r.IsReserved)); - Assert.That(results, Has.All.Matches(r => r.ParentRange == results.First().ParentRange)); + // All the results should be reserved ranges from the same parent allocation. + Assert.That(reservations, Has.All.Matches(r => r.ParentAllocation == allocation)); // When we read over the ranges, we should get the same value we stored. Assert.That(readBackValues, Is.All.EqualTo(value)); @@ -123,18 +126,18 @@ public void StoreWithoutRangeWithMultipleSmallValuesTest() [Test] public void StoreWithMultipleOverflowingValuesTest() { - var range = TestProcessMemory!.Allocate(0x1000, false); - var value = new byte[range.Range.GetSize()]; + var allocation = TestProcessMemory!.Allocate(0x1000, false); + var value = new byte[allocation.Range.GetSize()]; TestProcessMemory!.Store(value); // So far, we should have only one allocated range. - Assert.That(TestProcessMemory!.AllocatedRanges, Has.Count.EqualTo(1)); + Assert.That(TestProcessMemory!.Allocations, Has.Count.EqualTo(1)); // Now we store the same value again, which should overflow the range. TestProcessMemory!.Store(value); // We should have two allocated ranges now, because there is no room left in the first range. - Assert.That(TestProcessMemory!.AllocatedRanges, Has.Count.EqualTo(2)); + Assert.That(TestProcessMemory!.Allocations, Has.Count.EqualTo(2)); } } \ No newline at end of file diff --git a/test/MindControl.Test/RangeTests/AllocatedRangeTest.cs b/test/MindControl.Test/RangeTests/AllocatedRangeTest.cs deleted file mode 100644 index 9e1d2dd..0000000 --- a/test/MindControl.Test/RangeTests/AllocatedRangeTest.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.ComponentModel; -using MindControl.Test.ProcessMemoryTests; -using NUnit.Framework; - -namespace MindControl.Test.RangeTests; - -/// -/// Tests the class. -/// Because this class is strongly bound to a ProcessMemory, we have to use the -/// method to create instances, so the tests below will use an actual instance of and -/// depend on that method. -/// -[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class AllocatedRangeTest : ProcessMemoryTest -{ - private AllocatedRange _range; - - /// - /// Initializes the test by allocating a range of memory. - /// - [SetUp] - public void SetUp() - { - // Allocate a range of memory for the tests - _range = TestProcessMemory!.Allocate(0x1000, false); - } - - /// - /// Tests the method. - /// Expects the range to be removed from the list of allocated ranges and the memory to be released. - /// - [Test] - public void DisposeTest() - { - var address = _range.Range.Start; - _range.Dispose(); - - // Check that the range is now unusable - Assert.That(_range.IsReserved, Is.False); - Assert.Throws(() => _range.ReserveRange(0x10)); - - // Check that the range has been removed from the list of allocated ranges - Assert.That(TestProcessMemory!.AllocatedRanges, Is.Empty); - - // Check that the memory has been released (we should not be able to write to it) - Assert.Throws(() => TestProcessMemory.Write(address, 0, MemoryProtectionStrategy.Ignore)); - } - - /// - /// Tests the method on a reservation. - /// Expects the range to be removed from the list of the parent allocated range. - /// - [Test] - public void DisposeReservationTest() - { - var reservation = _range.ReserveRange(0x10); - reservation.Dispose(); - Assert.That(reservation.IsReserved, Is.False); - Assert.That(_range.ReservedRanges, Is.Empty); - Assert.That(_range.GetRemainingSpace(), Is.EqualTo(_range.Range.GetSize())); - } - - /// - /// Tests the method. - /// Reserve a single 0x10 portion of memory in the range. - /// - [Test] - public void ReserveTest() - { - var reservedRange = _range.ReserveRange(0x10); - - // Check the reserved range - Assert.That(reservedRange, Is.Not.Null); - Assert.That(reservedRange.Range.GetSize(), Is.EqualTo(0x10)); - Assert.That(reservedRange.IsReserved, Is.True); - Assert.That(reservedRange.ParentRange, Is.EqualTo(_range)); - Assert.That(reservedRange.ReservedRanges, Is.Empty); - Assert.That(reservedRange.GetRemainingSpace(), Is.EqualTo(0x10)); - Assert.That(reservedRange.GetTotalReservedSpace(), Is.Zero); - Assert.That(reservedRange.GetLargestReservableSpace()?.GetSize(), Is.EqualTo(0x10)); - - // Check the effect on the parent range - Assert.That(_range.ReservedRanges, Has.Member(reservedRange)); - Assert.That(_range.ReservedRanges, Has.Count.EqualTo(1)); - } - - /// - /// Tests the method, before and after reservations. - /// - [Test] - public void GetRemainingSpaceTest() - { - Assert.That(_range.GetRemainingSpace(), Is.EqualTo(_range.Range.GetSize())); - _range.ReserveRange(0x10); - var b = _range.ReserveRange(0x10); - _range.ReserveRange(0x10); - Assert.That(_range.GetRemainingSpace(), Is.EqualTo(_range.Range.GetSize() - 0x30)); - - b.Dispose(); - Assert.That(_range.GetRemainingSpace(), Is.EqualTo(_range.Range.GetSize() - 0x20)); - } -} From 58938a79271766637fa8596409181a60bcfc07fa Mon Sep 17 00:00:00 2001 From: Doublevil Date: Mon, 6 May 2024 20:35:37 +0200 Subject: [PATCH 06/66] First result implementation --- .../MindControl.csproj.DotSettings | 3 +- .../ProcessMemory.CodeInjection.cs | 7 ++ .../ProcessMemory/ProcessMemory.cs | 7 -- src/MindControl/Result/McResult.cs | 93 +++++++++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 src/MindControl/Result/McResult.cs diff --git a/src/MindControl/MindControl.csproj.DotSettings b/src/MindControl/MindControl.csproj.DotSettings index 525d991..50f7c7c 100644 --- a/src/MindControl/MindControl.csproj.DotSettings +++ b/src/MindControl/MindControl.csproj.DotSettings @@ -2,4 +2,5 @@ True True True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs b/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs index c447f83..e7640b2 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs @@ -6,6 +6,13 @@ namespace MindControl; // This partial class implements methods related to code injection. public partial class ProcessMemory { + /// + /// Gets or sets the time to wait for the spawned thread to return when injecting a library using the + /// method. + /// By default, this value will be set to 10 seconds. + /// + public TimeSpan LibraryInjectionThreadTimeout { get; set; } = TimeSpan.FromSeconds(10); + /// /// Injects a library into the attached process. /// diff --git a/src/MindControl/ProcessMemory/ProcessMemory.cs b/src/MindControl/ProcessMemory/ProcessMemory.cs index 98fba12..a33abac 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.cs @@ -29,13 +29,6 @@ public partial class ProcessMemory : IDisposable /// By default, this value will be . /// public MemoryProtectionStrategy DefaultWriteStrategy { get; set; } = MemoryProtectionStrategy.RemoveAndRestore; - - /// - /// Gets or sets the time to wait for the spawned thread to return when injecting a library using the - /// method. - /// By default, this value will be set to 10 seconds. - /// - public TimeSpan LibraryInjectionThreadTimeout { get; set; } = TimeSpan.FromSeconds(10); /// /// Event raised when the process detaches for any reason. diff --git a/src/MindControl/Result/McResult.cs b/src/MindControl/Result/McResult.cs new file mode 100644 index 0000000..cf2c70b --- /dev/null +++ b/src/MindControl/Result/McResult.cs @@ -0,0 +1,93 @@ +namespace MindControl; + +/// +/// Represents the result of an operation that can either succeed or fail. +/// +/// Type of the error that can be returned in case of failure. +public class McResult +{ + /// + /// Gets the error that caused the operation to fail, if any. + /// + public TError? Error { get; } + + /// + /// Gets the error message that describes the error that caused the operation to fail, if any. + /// + public string? ErrorMessage { get; } + + /// + /// Gets a boolean indicating if the operation was successful. + /// + public bool IsSuccess { get; } + + /// + /// Initializes a new successful instance. + /// + protected McResult() { IsSuccess = true; } + + /// + /// Initializes a new failed instance. + /// + /// Error that caused the operation to fail. + /// Error message that describes the error that caused the operation to fail. + protected McResult(TError error, string? errorMessage) + { + Error = error; + ErrorMessage = errorMessage; + IsSuccess = false; + } + + /// + /// Creates a new successful instance. + /// + public static McResult Success() => new(); + + /// + /// Creates a new failed instance. + /// + /// Error that caused the operation to fail. + /// Error message that describes the error that caused the operation to fail. + public static McResult Failure(TError error, string? errorMessage = null) => new(error, errorMessage); +} + +/// +/// Represents the result of an operation that can either succeed or fail, with a result value in case of success. +/// +/// Type of the result that can be returned in case of success. +/// Type of the error that can be returned in case of failure. +public sealed class McResult : McResult +{ + /// + /// Gets the result of the operation, if any. + /// + public TResult? Result { get; } + + /// + /// Initializes a new successful instance. + /// + /// Result of the operation. + private McResult(TResult result) { Result = result; } + + /// + /// Initializes a new failed instance. + /// + /// Error that caused the operation to fail. + /// Error message that describes the error that caused the operation to fail. + private McResult(TError error, string? errorMessage) : base(error, errorMessage) { } + + /// + /// Creates a new successful instance. + /// + /// Result of the operation. + public static McResult Success(TResult result) + => new(result); + + /// + /// Creates a new failed instance. + /// + /// Error that caused the operation to fail. + /// Error message that describes the error that caused the operation to fail. + public new static McResult Failure(TError error, string? errorMessage = null) + => new(error, errorMessage); +} From d18ff4737fccb3d66d05b9724bed917c08c46aef Mon Sep 17 00:00:00 2001 From: Doublevil Date: Mon, 20 May 2024 14:28:54 +0200 Subject: [PATCH 07/66] Full Result implementation on the ProcessMemory API --- src/MindControl/Addressing/MemoryRange.cs | 4 + .../Allocation/MemoryAllocation.cs | 28 +- .../InsufficientAllocatedMemoryException.cs | 30 -- src/MindControl/MemoryException.cs | 20 - .../MindControl.csproj.DotSettings | 3 +- .../Native/IOperatingSystemService.cs | 39 +- .../Native/Win32Service.Imports.cs | 8 +- src/MindControl/Native/Win32Service.cs | 197 +++++---- .../ProcessMemory/ProcessMemory.Addressing.cs | 87 ++-- .../ProcessMemory/ProcessMemory.Allocation.cs | 123 ++++-- .../ProcessMemory.CodeInjection.cs | 113 +++-- .../ProcessMemory/ProcessMemory.FindBytes.cs | 132 +----- .../ProcessMemory/ProcessMemory.Read.cs | 391 +++++++++++------- .../ProcessMemory/ProcessMemory.Write.cs | 258 ++++++++---- .../ProcessMemory/ProcessMemory.cs | 10 +- src/MindControl/Result/McResult.cs | 93 ----- src/MindControl/Results/AllocationFailure.cs | 53 +++ src/MindControl/Results/FindBytesFailure.cs | 13 + src/MindControl/Results/InjectionFailure.cs | 84 ++++ .../Results/PathEvaluationFailure.cs | 95 +++++ src/MindControl/Results/ReadFailure.cs | 103 +++++ src/MindControl/Results/ReservationFailure.cs | 31 ++ src/MindControl/Results/Result.cs | 160 +++++++ .../Results/ResultFailureException.cs | 32 ++ src/MindControl/Results/SystemFailure.cs | 51 +++ src/MindControl/Results/WriteFailure.cs | 131 ++++++ src/MindControl/Search/ByteSearchPattern.cs | 150 +++++++ .../AddressingTests/MemoryRangeTest.cs | 4 +- .../AllocationTests/MemoryAllocationTest.cs | 66 +-- .../AllocationTests/MemoryReservationTest.cs | 4 +- .../ProcessMemoryAllocationTest.cs | 41 +- .../ProcessMemoryEvaluateTest.cs | 93 ++++- .../ProcessMemoryInjectionTest.cs | 26 +- .../ProcessMemoryReadTest.cs | 89 ++-- .../ProcessMemoryWriteTest.cs | 8 +- .../SearchTests/ByteSearchPatternTest.cs | 102 +++++ 36 files changed, 2029 insertions(+), 843 deletions(-) delete mode 100644 src/MindControl/InsufficientAllocatedMemoryException.cs delete mode 100644 src/MindControl/MemoryException.cs delete mode 100644 src/MindControl/Result/McResult.cs create mode 100644 src/MindControl/Results/AllocationFailure.cs create mode 100644 src/MindControl/Results/FindBytesFailure.cs create mode 100644 src/MindControl/Results/InjectionFailure.cs create mode 100644 src/MindControl/Results/PathEvaluationFailure.cs create mode 100644 src/MindControl/Results/ReadFailure.cs create mode 100644 src/MindControl/Results/ReservationFailure.cs create mode 100644 src/MindControl/Results/Result.cs create mode 100644 src/MindControl/Results/ResultFailureException.cs create mode 100644 src/MindControl/Results/SystemFailure.cs create mode 100644 src/MindControl/Results/WriteFailure.cs create mode 100644 src/MindControl/Search/ByteSearchPattern.cs create mode 100644 test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs diff --git a/src/MindControl/Addressing/MemoryRange.cs b/src/MindControl/Addressing/MemoryRange.cs index b87ce5e..8d8be83 100644 --- a/src/MindControl/Addressing/MemoryRange.cs +++ b/src/MindControl/Addressing/MemoryRange.cs @@ -150,6 +150,10 @@ public void Deconstruct(out UIntPtr start, out UIntPtr end) start = Start; end = End; } + + /// Returns the fully qualified type name of this instance. + /// The fully qualified type name. + public override string ToString() => $"[{Start:X}, {End:X}]"; } /// diff --git a/src/MindControl/Allocation/MemoryAllocation.cs b/src/MindControl/Allocation/MemoryAllocation.cs index 01fd5a7..e17aabf 100644 --- a/src/MindControl/Allocation/MemoryAllocation.cs +++ b/src/MindControl/Allocation/MemoryAllocation.cs @@ -1,4 +1,6 @@ -namespace MindControl; +using MindControl.Results; + +namespace MindControl; /// /// Represents a range of memory that has been allocated in a process. @@ -139,39 +141,21 @@ private IEnumerable GetFreeRanges() /// /// Reserves and returns the next available range with the specified size. /// Data stored in the returned range will not be overwritten by future reservations until the reservation is freed. - /// This method throws if there is no contiguous memory large enough in the range. See - /// for a non-throwing alternative. /// /// Minimal size that the range to get must be able to accomodate, in bytes. /// Optional byte alignment for the range. When null, values are not aligned. The /// default value is 8, meaning that for example a range of [0x15,0x3C] will be aligned to [0x18,0x38] and thus /// only accomodate 32 bytes. Alignment means the actual reserved space might be bigger than the /// , but will never make it smaller. - /// The resulting reservation. - public MemoryReservation ReserveRange(ulong size, uint? byteAlignment = 8) - => TryReserveRange(size, byteAlignment) ?? throw new InsufficientAllocatedMemoryException(size, byteAlignment); - - /// - /// Attempts to reserve and return the next available range with the specified size. - /// Data stored in the returned range will not be overwritten by future reservations until the reservation is freed. - /// This method returns null if there is no contiguous memory large enough in the range. See - /// if you expect an exception to be thrown on a reservation failure. - /// Note that this method will still throw if the range has been disposed. - /// - /// Minimal size that the range to get must be able to accomodate, in bytes. - /// Optional byte alignment for the range. When null, values are not aligned. The - /// default value is 8, meaning that for example a range of [0x15,0x3C] will be aligned to [0x18,0x38] and thus - /// only accomodate 32 bytes. Alignment means the actual reserved space might be bigger than the - /// , but will never make it smaller. - /// The resulting reservation if possible, null otherwise. - public MemoryReservation? TryReserveRange(ulong size, uint? byteAlignment = 8) + /// A result holding the resulting reservation or a reservation failure. + public Result ReserveRange(ulong size, uint? byteAlignment = 8) { if (IsDisposed) throw new ObjectDisposedException(nameof(MemoryAllocation)); var range = GetNextRangeFittingSize(size, byteAlignment); if (range == null) - return null; + return new ReservationFailureOnNoSpaceAvailable(); var reservedRange = new MemoryReservation(range.Value, this); _reservations.Add(reservedRange); diff --git a/src/MindControl/InsufficientAllocatedMemoryException.cs b/src/MindControl/InsufficientAllocatedMemoryException.cs deleted file mode 100644 index 2fa33cc..0000000 --- a/src/MindControl/InsufficientAllocatedMemoryException.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace MindControl; - -/// -/// Exception thrown by a when trying to reserve a memory range with a size exceeding -/// the largest contiguous unreserved space. -/// -public class InsufficientAllocatedMemoryException : Exception -{ - /// - /// Gets the requested size for the reservation attempt that caused the exception. - /// - public ulong RequestedSize { get; } - - /// - /// Gets the requested byte alignment for the reservation attempt that caused the exception. - /// - public uint? RequestedAlignment { get; } - - /// - /// Builds a new with the given parameters. - /// - /// Requested size for the reservation attempt. - /// Requested byte alignment for the reservation attempt. - public InsufficientAllocatedMemoryException(ulong requestedSize, uint? requestedAlignment) - : base($"The requested size of {requestedSize} bytes with {(requestedAlignment == null ? "no alignment" : $"an alignment of {requestedAlignment.Value}")} bytes exceeds the largest contiguous unreserved space of the {nameof(MemoryAllocation)} instance. Consider allocating more space, reserving multiple smaller blocks, or letting the {nameof(ProcessMemory)} instance handle allocations by using addressless Write method signatures. Read the \"Allocating memory\" section in the documentation for more information.") - { - RequestedSize = requestedSize; - RequestedAlignment = requestedAlignment; - } -} \ No newline at end of file diff --git a/src/MindControl/MemoryException.cs b/src/MindControl/MemoryException.cs deleted file mode 100644 index 01e1d9e..0000000 --- a/src/MindControl/MemoryException.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MindControl; - -/// -/// Exception related to a memory manipulation operation. -/// -public class MemoryException : Exception -{ - /// - /// Builds a with the given details. - /// - /// Error message that explains the reason for the exception. - public MemoryException(string message) : base(message) {} - - /// - /// Builds a with the given details. - /// - /// Error message that explains the reason for the exception. - /// Exception that is the cause of this exception, if any. - public MemoryException(string message, Exception? innerException) : base(message, innerException) {} -} \ No newline at end of file diff --git a/src/MindControl/MindControl.csproj.DotSettings b/src/MindControl/MindControl.csproj.DotSettings index 50f7c7c..bb9a772 100644 --- a/src/MindControl/MindControl.csproj.DotSettings +++ b/src/MindControl/MindControl.csproj.DotSettings @@ -3,4 +3,5 @@ True True True - True \ No newline at end of file + False + True \ No newline at end of file diff --git a/src/MindControl/Native/IOperatingSystemService.cs b/src/MindControl/Native/IOperatingSystemService.cs index 893ce9f..3bd632d 100644 --- a/src/MindControl/Native/IOperatingSystemService.cs +++ b/src/MindControl/Native/IOperatingSystemService.cs @@ -1,4 +1,6 @@ -namespace MindControl.Native; +using MindControl.Results; + +namespace MindControl.Native; /// /// Provides process-related features. @@ -10,14 +12,14 @@ public interface IOperatingSystemService /// /// Identifier of the target process. /// Handle of the opened process. - IntPtr OpenProcess(int pid); + Result OpenProcess(int pid); /// /// Returns a value indicating if the process with the given identifier is a 64-bit process or not. /// /// Identifier of the target process. /// True if the process is 64-bits, false otherwise. - bool IsProcess64Bits(int pid); + Result IsProcess64Bits(int pid); /// /// Reads a targeted range of the memory of a specified process. @@ -26,7 +28,7 @@ public interface IOperatingSystemService /// Starting address of the memory range to read. /// Length of the memory range to read. /// An array of bytes containing the data read from the memory. - byte[]? ReadProcessMemory(IntPtr processHandle, UIntPtr baseAddress, ulong length); + Result ReadProcessMemory(IntPtr processHandle, UIntPtr baseAddress, ulong length); /// /// Overwrites the memory protection of the page that the given address is part of. @@ -40,7 +42,7 @@ public interface IOperatingSystemService /// The memory protection value that was effective on the page before being changed. /// The process handle is invalid (zero pointer). /// The target address is invalid (zero pointer). - MemoryProtection ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bits, UIntPtr targetAddress, + Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bits, UIntPtr targetAddress, MemoryProtection newProtection); /// @@ -53,7 +55,7 @@ MemoryProtection ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bits, /// written, unless a size is specified. /// Specify this value if you only want to write part of the value array in memory. /// This parameter is useful when using buffer byte arrays. Leave it to null to use the entire array. - void WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, byte[] value, int? size = null); + Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, byte[] value, int? size = null); /// /// Allocates memory in the specified process. @@ -63,7 +65,7 @@ MemoryProtection ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bits, /// Type of memory allocation. /// Protection flags of the memory to allocate. /// A pointer to the start of the allocated memory. - UIntPtr AllocateMemory(IntPtr processHandle, int size, MemoryAllocationType allocationType, + Result AllocateMemory(IntPtr processHandle, int size, MemoryAllocationType allocationType, MemoryProtection protection); /// @@ -75,13 +77,13 @@ UIntPtr AllocateMemory(IntPtr processHandle, int size, MemoryAllocationType allo /// Type of memory allocation. /// Protection flags of the memory to allocate. /// A pointer to the start of the allocated memory. - UIntPtr AllocateMemory(IntPtr processHandle, UIntPtr address, int size, MemoryAllocationType allocationType, + Result AllocateMemory(IntPtr processHandle, UIntPtr address, int size, MemoryAllocationType allocationType, MemoryProtection protection); /// /// Gets the address of the function used to load a library in the current process. /// - UIntPtr GetLoadLibraryFunctionAddress(); + Result GetLoadLibraryFunctionAddress(); /// /// Spawns a thread in the specified process, starting at the given address. @@ -90,30 +92,30 @@ UIntPtr AllocateMemory(IntPtr processHandle, UIntPtr address, int size, MemoryAl /// Address of the start routine to be executed by the thread. /// Address of any parameter to be passed to the start routine. /// Handle of the thread. - IntPtr CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, UIntPtr parameterAddress); + Result CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, UIntPtr parameterAddress); /// /// Waits for the specified thread to finish execution. /// /// Handle of the target thread. /// Maximum time to wait for the thread to finish. - /// True if the thread finished execution, false if the timeout was reached. Other failures will throw an - /// exception. - bool WaitThread(IntPtr threadHandle, TimeSpan timeout); + /// True if the thread finished execution, false if the timeout was reached. Other failures will return a + /// failure. + Result WaitThread(IntPtr threadHandle, TimeSpan timeout); /// /// Frees the memory allocated in the specified process for a region or a placeholder. /// /// Handle of the target process. - /// Base address of the region or placeholder to free, as returned by - /// . - void ReleaseMemory(IntPtr processHandle, UIntPtr regionBaseAddress); + /// Base address of the region or placeholder to free, as returned by the allocation + /// methods. + Result ReleaseMemory(IntPtr processHandle, UIntPtr regionBaseAddress); /// /// Closes the given handle. /// /// Handle to close. - void CloseHandle(IntPtr handle); + Result CloseHandle(IntPtr handle); /// /// Gets the range of memory addressable by applications in the current system. @@ -127,7 +129,8 @@ UIntPtr AllocateMemory(IntPtr processHandle, UIntPtr address, int size, MemoryAl /// Base address of the target memory region. /// A boolean indicating if the target process is 64 bits or not. /// If left null, the method will automatically determine the bitness of the process. - MemoryRangeMetadata GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress, bool? is64Bits = null); + Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress, + bool is64Bits); /// /// Gets the allocation granularity (minimal allocation size) of the system. diff --git a/src/MindControl/Native/Win32Service.Imports.cs b/src/MindControl/Native/Win32Service.Imports.cs index 66771f4..052ae84 100644 --- a/src/MindControl/Native/Win32Service.Imports.cs +++ b/src/MindControl/Native/Win32Service.Imports.cs @@ -10,7 +10,7 @@ public partial class Win32Service /// Retrieves information about the current system. /// /// A pointer to a SYSTEM_INFO structure that receives the information. - [DllImport("kernel32.dll")] + [DllImport("kernel32.dll", SetLastError = true)] private static extern void GetSystemInfo(out SystemInfo lpSystemInfo); /// @@ -287,7 +287,7 @@ private static extern UIntPtr VirtualAllocEx(IntPtr hProcess, UIntPtr lpAddress, /// process. /// If the function succeeds, the return value is a handle to the specified module. If the function fails, /// the return value is NULL. To get extended error information, call GetLastError. - [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr GetModuleHandle(string lpModuleName); /// @@ -423,7 +423,7 @@ private static extern int ReadProcessMemory(IntPtr hProcess, UIntPtr lpBaseAddre /// A pointer to a variable that receives the previous access protection of the first /// page in the specified region of pages. /// If the function succeeds, the return value is true. Otherwise, it will be false. - [DllImport("kernel32.dll")] + [DllImport("kernel32.dll", SetLastError = true)] public static extern bool VirtualProtectEx(IntPtr hProcess, UIntPtr lpAddress, IntPtr dwSize, MemoryProtection flNewProtect, out MemoryProtection lpflOldProtect); @@ -442,7 +442,7 @@ public static extern bool VirtualProtectEx(IntPtr hProcess, UIntPtr lpAddress, /// A pointer to a variable that receives the number of bytes transferred into /// the specified process. This parameter is optional. If null, it will be ignored. /// If the function succeeds, the return value is true. Otherwise, it will be false. - [DllImport("kernel32.dll")] + [DllImport("kernel32.dll", SetLastError = true)] public static extern bool WriteProcessMemory(IntPtr hProcess, UIntPtr lpBaseAddress, byte[] lpBuffer, UIntPtr nSize, IntPtr lpNumberOfBytesWritten); } \ No newline at end of file diff --git a/src/MindControl/Native/Win32Service.cs b/src/MindControl/Native/Win32Service.cs index 5d1e0f0..dee90e1 100644 --- a/src/MindControl/Native/Win32Service.cs +++ b/src/MindControl/Native/Win32Service.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; +using MindControl.Results; namespace MindControl.Native; @@ -11,16 +12,25 @@ public partial class Win32Service : IOperatingSystemService { private SystemInfo? _systemInfo; + /// + /// Builds and returns a failure object representing the last Win32 error that occurred. + /// + private static OperatingSystemCallFailure GetLastSystemError() + { + int errorCode = Marshal.GetLastWin32Error(); + return new OperatingSystemCallFailure(errorCode, new Win32Exception(errorCode).Message); + } + /// /// Opens the process with the given identifier, in a way that allows memory manipulation. /// /// Identifier of the target process. /// Handle of the opened process. - public IntPtr OpenProcess(int pid) + public Result OpenProcess(int pid) { var handle = OpenProcess(0x1F0FFF, true, pid); if (handle == IntPtr.Zero) - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. + return GetLastSystemError(); return handle; } @@ -30,20 +40,24 @@ public IntPtr OpenProcess(int pid) /// /// Identifier of the target process. /// True if the process is 64-bits, false otherwise. - public bool IsProcess64Bits(int pid) + public Result IsProcess64Bits(int pid) { - var process = Process.GetProcessById(pid); - if (process == null) - throw new ArgumentException($"Process {pid} was not found."); - - if (!IsWow64Process(process.Handle, out bool isWow64)) - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. - process.Dispose(); + try + { + using var process = Process.GetProcessById(pid); + if (!IsWow64Process(process.Handle, out bool isWow64)) + return GetLastSystemError(); - bool isSystem64Bits = IntPtr.Size == 8; + bool isSystem64Bits = IntPtr.Size == 8; - // Process is 64 bits if we are running a 64-bits system and the process is NOT in wow64. - return !isWow64 && isSystem64Bits; + // Process is 64 bits if we are running a 64-bits system and the process is NOT in wow64. + return !isWow64 && isSystem64Bits; + } + catch (Exception) + { + return new SystemFailureOnInvalidArgument(nameof(pid), + $"The process of PID {pid} was not found. Check that the process is running."); + } } /// @@ -53,36 +67,16 @@ public bool IsProcess64Bits(int pid) /// Starting address of the memory range to read. /// Length of the memory range to read. /// An array of bytes containing the data read from the memory. - public byte[]? ReadProcessMemory(IntPtr processHandle, UIntPtr baseAddress, ulong length) + public Result ReadProcessMemory(IntPtr processHandle, UIntPtr baseAddress, ulong length) { if (processHandle == IntPtr.Zero) - throw new ArgumentException("The process handle is invalid (zero pointer).", nameof(processHandle)); + return new SystemFailureOnInvalidArgument(nameof(processHandle), + "The process handle is invalid (zero pointer)."); var result = new byte[length]; int returnValue = ReadProcessMemory(processHandle, baseAddress, result, length, out _); - if (returnValue == 0) - { - int errorCode = Marshal.GetLastWin32Error(); - - // ERROR_PARTIAL_COPY (299): Generic error that is raised when the address isn't valid for whatever reason. - // This error is quite generic and does not really allow users to identify what's wrong. - // In order to simplify error handling by a significant margin and also preserve performance, we will - // not throw when getting this particular error code. - if (errorCode == 299) - return null; - - // ERROR_NOACCESS (998): Error raised when the memory we are trying to read is protected for whatever - // reason. Since this can be due to trying to access an invalid address, for the same reasons as noted - // above (error 299), we will not throw. This behaviour might change or be configurable in later releases. - if (errorCode == 998) - return null; - - // In other cases, throw. - throw new Win32Exception(errorCode); - } - - return result; + return returnValue == 0 ? GetLastSystemError() : result; } /// @@ -95,21 +89,20 @@ public bool IsProcess64Bits(int pid) /// An address in the target page. /// New protection value for the page. /// The memory protection value that was effective on the page before being changed. - public MemoryProtection ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bits, UIntPtr targetAddress, - MemoryProtection newProtection) + public Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bits, + UIntPtr targetAddress, MemoryProtection newProtection) { if (processHandle == IntPtr.Zero) - throw new ArgumentException("The process handle is invalid (zero pointer).", nameof(processHandle)); + return new SystemFailureOnInvalidArgument(nameof(processHandle), + "The process handle is invalid (zero pointer)."); if (targetAddress == UIntPtr.Zero) - throw new ArgumentOutOfRangeException(nameof(targetAddress),"The target address cannot be a zero pointer."); + return new SystemFailureOnInvalidArgument(nameof(targetAddress), + "The target address cannot be a zero pointer."); bool result = VirtualProtectEx(processHandle, targetAddress, (IntPtr)(is64Bits ? 8 : 4), newProtection, out var previousProtection); - if (!result) - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. - - return previousProtection; + return result ? previousProtection : GetLastSystemError(); } /// @@ -122,20 +115,22 @@ public MemoryProtection ReadAndOverwriteProtection(IntPtr processHandle, bool is /// written, unless a size is specified. /// Specify this value if you only want to write part of the value array in memory. /// This parameter is useful when using buffer byte arrays. Leave it to null to use the entire array. - public void WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, byte[] value, int? size = null) + public Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, byte[] value, int? size = null) { if (processHandle == IntPtr.Zero) - throw new ArgumentException("The process handle is invalid (zero pointer).", nameof(processHandle)); + return new SystemFailureOnInvalidArgument(nameof(processHandle), + "The process handle is invalid (zero pointer)."); if (targetAddress == UIntPtr.Zero) - throw new ArgumentOutOfRangeException(nameof(targetAddress),"The target address cannot be a zero pointer."); + return new SystemFailureOnInvalidArgument(nameof(targetAddress), + "The target address cannot be a zero pointer."); if (size != null && size.Value > value.Length) - throw new ArgumentOutOfRangeException(nameof(size),"The size cannot exceed the length of the value array."); + return new SystemFailureOnInvalidArgument(nameof(size), + "The size cannot exceed the length of the value array."); - bool result = WriteProcessMemory(processHandle, targetAddress, value, - (UIntPtr)(size ?? value.Length), IntPtr.Zero); - - if (!result) - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. + bool result = WriteProcessMemory(processHandle, targetAddress, value, (UIntPtr)(size ?? value.Length), + IntPtr.Zero); + + return result ? Result.Success : GetLastSystemError(); } /// @@ -146,8 +141,9 @@ public void WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, byte /// Type of memory allocation. /// Protection flags of the memory to allocate. /// A pointer to the start of the allocated memory. - public UIntPtr AllocateMemory(IntPtr processHandle, int size, MemoryAllocationType allocationType, - MemoryProtection protection) => AllocateMemory(processHandle, UIntPtr.Zero, size, allocationType, protection); + public Result AllocateMemory(IntPtr processHandle, int size, + MemoryAllocationType allocationType, MemoryProtection protection) + => AllocateMemory(processHandle, UIntPtr.Zero, size, allocationType, protection); /// /// Allocates memory in the specified process at the specified address. @@ -158,19 +154,17 @@ public UIntPtr AllocateMemory(IntPtr processHandle, int size, MemoryAllocationTy /// Type of memory allocation. /// Protection flags of the memory to allocate. /// A pointer to the start of the allocated memory. - public UIntPtr AllocateMemory(IntPtr processHandle, UIntPtr address, int size, MemoryAllocationType allocationType, - MemoryProtection protection) + public Result AllocateMemory(IntPtr processHandle, UIntPtr address, int size, + MemoryAllocationType allocationType, MemoryProtection protection) { if (processHandle == IntPtr.Zero) - throw new ArgumentException("The process handle is invalid (zero pointer).", nameof(processHandle)); + return new SystemFailureOnInvalidArgument(nameof(processHandle), + "The process handle is invalid (zero pointer)."); if (size <= 0) - throw new ArgumentOutOfRangeException(nameof(size),"The size to allocate must be strictly positive."); + return new SystemFailureOnInvalidArgument(nameof(size), "The size to allocate must be strictly positive."); var result = VirtualAllocEx(processHandle, address, (uint)size, (uint)allocationType, (uint)protection); - if (result == UIntPtr.Zero) - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. - - return result; + return result == UIntPtr.Zero ? GetLastSystemError() : result; } /// @@ -179,15 +173,15 @@ public UIntPtr AllocateMemory(IntPtr processHandle, UIntPtr address, int size, M /// Name or path of the module. This module must be loaded in the current process. /// Name of the target function in the specified module. /// The address of the function if located, or null otherwise. - private UIntPtr GetFunctionAddress(string moduleName, string functionName) + private Result GetFunctionAddress(string moduleName, string functionName) { var moduleHandle = GetModuleHandle(moduleName); if (moduleHandle == IntPtr.Zero) - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. + return GetLastSystemError(); var functionAddress = GetProcAddress(moduleHandle, functionName); if (functionAddress == UIntPtr.Zero) - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. + return GetLastSystemError(); return functionAddress; } @@ -195,7 +189,8 @@ private UIntPtr GetFunctionAddress(string moduleName, string functionName) /// /// Gets the address of the function used to load a library in the current process. /// - public UIntPtr GetLoadLibraryFunctionAddress() => GetFunctionAddress("kernel32.dll", "LoadLibraryW"); + public Result GetLoadLibraryFunctionAddress() + => GetFunctionAddress("kernel32.dll", "LoadLibraryW"); /// /// Spawns a thread in the specified process, starting at the given address. @@ -204,16 +199,19 @@ private UIntPtr GetFunctionAddress(string moduleName, string functionName) /// Address of the start routine to be executed by the thread. /// Address of any parameter to be passed to the start routine. /// Handle of the thread. - public IntPtr CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, UIntPtr parameterAddress) + public Result CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, + UIntPtr parameterAddress) { if (processHandle == IntPtr.Zero) - throw new ArgumentException("The process handle is invalid (zero pointer).", nameof(processHandle)); + return new SystemFailureOnInvalidArgument(nameof(processHandle), + "The process handle is invalid (zero pointer)."); if (startAddress == UIntPtr.Zero) - throw new ArgumentException("The start address is invalid (zero pointer).", nameof(startAddress)); + return new SystemFailureOnInvalidArgument(nameof(startAddress), + "The start address is invalid (zero pointer)."); var result = CreateRemoteThread(processHandle, IntPtr.Zero, 0, startAddress, parameterAddress, 0, out _); if (result == IntPtr.Zero) - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. + return GetLastSystemError(); return result; } @@ -223,51 +221,52 @@ public IntPtr CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, UIn /// /// Handle of the target thread. /// Maximum time to wait for the thread to finish. - /// True if the thread finished execution, false if the timeout was reached. Other failures will throw an - /// exception. - public bool WaitThread(IntPtr threadHandle, TimeSpan timeout) + /// True if the thread finished execution, false if the timeout was reached. Other failures will return + /// a failure. + public Result WaitThread(IntPtr threadHandle, TimeSpan timeout) { if (threadHandle == IntPtr.Zero) - throw new ArgumentException("The thread handle is invalid (zero pointer).", nameof(threadHandle)); + return new SystemFailureOnInvalidArgument(nameof(threadHandle), + "The thread handle is invalid (zero pointer)."); uint result = WaitForSingleObject(threadHandle, (uint)timeout.TotalMilliseconds); if (WaitForSingleObjectResult.IsSuccessful(result)) return true; if (result == WaitForSingleObjectResult.Timeout) return false; - - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. + + return GetLastSystemError(); } /// /// Frees the memory allocated in the specified process for a region or a placeholder. /// /// Handle of the target process. - /// Base address of the region or placeholder to free, as returned by - /// . - public void ReleaseMemory(IntPtr processHandle, UIntPtr regionBaseAddress) + /// Base address of the region or placeholder to free, as returned by the memory + /// allocation methods. + public Result ReleaseMemory(IntPtr processHandle, UIntPtr regionBaseAddress) { if (processHandle == IntPtr.Zero) - throw new ArgumentException("The process handle is invalid (zero pointer).", nameof(processHandle)); + return new SystemFailureOnInvalidArgument(nameof(processHandle), + "The process handle is invalid (zero pointer)."); if (regionBaseAddress == UIntPtr.Zero) - throw new ArgumentException("The region base address is invalid (zero pointer).", - nameof(regionBaseAddress)); - - if (!VirtualFreeEx(processHandle, regionBaseAddress, 0, (uint)MemoryFreeType.Release)) - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. + return new SystemFailureOnInvalidArgument(nameof(regionBaseAddress), + "The region base address is invalid (zero pointer)."); + + return VirtualFreeEx(processHandle, regionBaseAddress, 0, (uint)MemoryFreeType.Release) + ? Result.Success : GetLastSystemError(); } /// /// Closes the given handle. /// /// Handle to close. - public void CloseHandle(IntPtr handle) + public Result CloseHandle(IntPtr handle) { if (handle == IntPtr.Zero) - throw new ArgumentException("The handle is invalid (zero pointer).", nameof(handle)); - - if (!WinCloseHandle(handle)) - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. + return new SystemFailureOnInvalidArgument(nameof(handle), "The handle is invalid (zero pointer)."); + + return WinCloseHandle(handle) ? Result.Success : GetLastSystemError(); } /// @@ -308,20 +307,18 @@ public MemoryRange GetFullMemoryRange() /// /// Handle of the target process. /// Base address of the target memory region. - /// A boolean indicating if the target process is 64 bits or not. - /// If left null, the method will automatically determine the bitness of the process. - public MemoryRangeMetadata GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress, bool? is64Bits = null) + /// A boolean indicating if the target process is 64 bits or not. + public Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress, + bool is64Bits) { - bool is64 = is64Bits ?? IsProcess64Bits(Process.GetCurrentProcess().Id); - MemoryBasicInformation memoryBasicInformation; - if (is64) + if (is64Bits) { // Use the 64-bit variant of the structure. var memInfo64 = new MemoryBasicInformation64(); if (VirtualQueryEx(processHandle, baseAddress, out memInfo64, (UIntPtr)Marshal.SizeOf(memInfo64)) == UIntPtr.Zero) - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself. + return GetLastSystemError(); memoryBasicInformation = new MemoryBasicInformation((UIntPtr)memInfo64.BaseAddress, (UIntPtr)memInfo64.AllocationBase, memInfo64.AllocationProtect, (UIntPtr)memInfo64.RegionSize, @@ -333,7 +330,7 @@ public MemoryRangeMetadata GetRegionMetadata(IntPtr processHandle, UIntPtr baseA var memInfo32 = new MemoryBasicInformation32(); if (VirtualQueryEx(processHandle, baseAddress, out memInfo32, (UIntPtr)Marshal.SizeOf(memInfo32)) == UIntPtr.Zero) - throw new Win32Exception(); // This constructor does all the job to retrieve the error by itself; + return GetLastSystemError(); memoryBasicInformation = new MemoryBasicInformation((UIntPtr)memInfo32.BaseAddress, (UIntPtr)memInfo32.AllocationBase, memInfo32.AllocationProtect, (UIntPtr)memInfo32.RegionSize, diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs index 8016fc3..7c5d07c 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using MindControl.Internal; +using MindControl.Results; namespace MindControl; @@ -11,24 +12,39 @@ public partial class ProcessMemory /// /// Pointer path to evaluate. /// The memory address pointed by the pointer path. - public UIntPtr? EvaluateMemoryAddress(PointerPath pointerPath) + public Result EvaluateMemoryAddress(PointerPath pointerPath) { if (pointerPath.IsStrictly64Bits && (IntPtr.Size == 4 || !_is64Bits)) - throw new ArgumentException( - $"The pointer path \"{pointerPath.Expression}\" uses addresses intended for a 64-bits process, but this instance is targeting a 32-bits process."); + return new PathEvaluationFailureOnIncompatibleBitness(); - UIntPtr? baseAddress = pointerPath.BaseModuleName != null - ? GetModuleAddress(pointerPath.BaseModuleName) - : pointerPath.PointerOffsets.FirstOrDefault().ToUIntPtr(); - - if (baseAddress == null) - return null; // Module not found + UIntPtr? baseAddress; + if (pointerPath.BaseModuleName != null) + { + baseAddress = GetModuleAddress(pointerPath.BaseModuleName); + if (baseAddress == null) + return new PathEvaluationFailureOnBaseModuleNotFound(pointerPath.BaseModuleName); + } + else + { + var bigIntAddress = pointerPath.PointerOffsets.FirstOrDefault(); + baseAddress = bigIntAddress.ToUIntPtr(); + if (baseAddress == null) + return new PathEvaluationFailureOnPointerOutOfRange(null, bigIntAddress); + } + // Apply the base offset if there is one if (pointerPath.BaseModuleOffset > 0) - baseAddress = (baseAddress.Value.ToUInt64() + pointerPath.BaseModuleOffset).ToUIntPtr(); + { + var bigIntAddress = baseAddress.Value.ToUInt64() + pointerPath.BaseModuleOffset; + baseAddress = bigIntAddress.ToUIntPtr(); + + if (baseAddress == null) + return new PathEvaluationFailureOnPointerOutOfRange(null, bigIntAddress); + } - if (baseAddress == null || baseAddress == UIntPtr.Zero) - return null; // Overflow after applying the module offset, or zero pointer + // Check if the base address is valid + if (baseAddress == UIntPtr.Zero) + return new PathEvaluationFailureOnPointerOutOfRange(null, 0); // Follow the pointer path offset by offset var currentAddress = baseAddress.Value; @@ -36,15 +52,24 @@ public partial class ProcessMemory for (int i = startIndex; i < pointerPath.PointerOffsets.Length; i++) { // Read the value pointed by the current address as a pointer address - UIntPtr? nextAddress = ReadIntPtr(currentAddress); - if (nextAddress == null) - return null; // Read operation failed on the address + var nextAddressResult = ReadIntPtr(currentAddress); + if (nextAddressResult.IsFailure) + return new PathEvaluationFailureOnPointerReadFailure(currentAddress, nextAddressResult.Error); + + var nextAddress = nextAddressResult.Value; // Apply the offset to the value we just read and check the result var offset = pointerPath.PointerOffsets[i]; - var nextValue = (nextAddress.Value.ToUInt64() + offset).ToUIntPtr(); - if (nextValue == null || nextValue == UIntPtr.Zero || !IsBitnessCompatible(nextValue.Value)) - return null; // Overflow after applying the offset; zero pointer; 64-bits pointer on 32-bits target + var nextValueBigInt = nextAddress.ToUInt64() + offset; + var nextValue = nextValueBigInt.ToUIntPtr(); + + // Check for invalid address values + if (nextValue == null) + return new PathEvaluationFailureOnPointerOutOfRange(currentAddress, nextValueBigInt); + if (nextValue == UIntPtr.Zero) + return new PathEvaluationFailureOnPointerOutOfRange(currentAddress, 0); + if (!IsBitnessCompatible(nextValue.Value)) + return new PathEvaluationFailureOnIncompatibleBitness(currentAddress); // The next value has been vetted. Keep going with it as the current address currentAddress = nextValue.Value; @@ -52,15 +77,6 @@ public partial class ProcessMemory return currentAddress; } - - /// - /// Evaluates the given pointer path to the memory address it points to in the process. - /// If the path does not evaluate to a proper address, throws a . - /// - /// Pointer path to evaluate. - /// The memory address pointed by the pointer path. - private UIntPtr EvaluateMemoryAddressOrThrow(PointerPath pointerPath) => EvaluateMemoryAddress(pointerPath) - ?? throw new MemoryException($"Could not evaluate pointer path \"{pointerPath}\"."); /// /// Gets the process module with the given name. @@ -117,4 +133,21 @@ private UIntPtr EvaluateMemoryAddressOrThrow(PointerPath pointerPath) => Evaluat return MemoryRange.FromStartAndSize((UIntPtr)(long)module.BaseAddress, (ulong)module.ModuleMemorySize - 1); } + + /// + /// Returns the intersection of the input range with the full addressable memory range. + /// If the input memory range is null, the full memory range is returned. + /// + /// Input memory range to clamp. If null, the full memory range is returned. + /// The clamped memory range, or the full memory range if the input is null. + private MemoryRange GetClampedMemoryRange(MemoryRange? input) + { + var fullMemoryRange = _osService.GetFullMemoryRange(); + return input == null ? fullMemoryRange : new MemoryRange( + Start: input.Value.Start.ToUInt64() < fullMemoryRange.Start.ToUInt64() + ? fullMemoryRange.Start : input.Value.Start, + End: input.Value.End.ToUInt64() > fullMemoryRange.End.ToUInt64() + ? fullMemoryRange.End : input.Value.End + ); + } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index 2dd0c3a..80d8a56 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -1,5 +1,6 @@ using MindControl.Internal; using MindControl.Native; +using MindControl.Results; namespace MindControl; @@ -15,25 +16,27 @@ public partial class ProcessMemory public IReadOnlyList Allocations => _allocations.AsReadOnly(); /// - /// Attempts to allocate a memory range of the given size within the process. + /// Attempts to allocate a memory range of the given size within the process. Use this method only when automatic + /// allocation management through the Store methods is not appropriate. /// /// Size of the memory range to allocate. /// Determines if the memory range can be used to store executable code. /// Specify this parameter to limit the allocation to a specific range of memory. - /// The allocated memory range. - public MemoryAllocation Allocate(ulong size, bool forExecutableCode, MemoryRange? limitRange = null) + /// A result holding either the allocated memory range, or an allocation failure. + public Result Allocate(ulong size, bool forExecutableCode, + MemoryRange? limitRange = null) { if (size == 0) throw new ArgumentException("The size of the memory range to allocate must be greater than zero.", nameof(size)); // Find a free memory range that satisfies the size needed - var range = FindAndAllocateFreeMemory(size, forExecutableCode, limitRange); - if (range == null) - throw new InvalidOperationException("No suitable free memory range was found."); + var rangeResult = FindAndAllocateFreeMemory(size, forExecutableCode, limitRange); + if (rangeResult.IsFailure) + return rangeResult.Error; // Add the range to the list of allocated ranges and return it - var allocatedRange = new MemoryAllocation(range.Value, forExecutableCode, this); + var allocatedRange = new MemoryAllocation(rangeResult.Value, forExecutableCode, this); _allocations.Add(allocatedRange); return allocatedRange; } @@ -43,7 +46,7 @@ public MemoryAllocation Allocate(ulong size, bool forExecutableCode, MemoryRange /// /// Allocation to free. /// This method is internal because it is designed to be called by . - /// Users would release memory by disposing instances. + /// Users would release memory by disposing instances. internal void Free(MemoryAllocation allocation) { if (!_allocations.Contains(allocation)) @@ -60,16 +63,18 @@ internal void Free(MemoryAllocation allocation) /// Set to true to allocate a memory block with execute permissions. /// Specify this parameter to limit the search to a specific range of memory. /// If left null (default), the entire process memory will be searched. - /// The start address of the free memory range, or null if no suitable range was found. - private MemoryRange? FindAndAllocateFreeMemory(ulong sizeNeeded, bool forExecutableCode, - MemoryRange? limitRange = null) + /// A result holding either the memory range found, or an allocation failure. + /// The reason why the method performs the allocation itself is because we cannot know if the range can + /// actually be allocated without performing the allocation. + private Result FindAndAllocateFreeMemory(ulong sizeNeeded, + bool forExecutableCode, MemoryRange? limitRange = null) { var maxRange = _osService.GetFullMemoryRange(); var actualRange = limitRange == null ? maxRange : maxRange.Intersect(limitRange.Value); // If the given range is not within the process applicative memory, return null if (actualRange == null) - return null; + return new AllocationFailureOnLimitRangeOutOfBounds(maxRange); // Compute the minimum multiple of the system page size that can fit the size needed // This will be the maximum size that we are going to allocate @@ -82,8 +87,8 @@ internal void Free(MemoryAllocation allocation) MemoryRange? freeRange = null; MemoryRangeMetadata currentMetadata; while (nextAddress.ToUInt64() < actualRange.Value.End.ToUInt64() - && (currentMetadata = _osService.GetRegionMetadata(_processHandle, nextAddress, _is64Bits)) - .Size.ToUInt64() > 0) + && (currentMetadata = _osService.GetRegionMetadata(_processHandle, nextAddress, _is64Bits) + .GetValueOrDefault()).Size.ToUInt64() > 0) { nextAddress = (UIntPtr)(nextAddress.ToUInt64() + currentMetadata.Size.ToUInt64()); @@ -110,24 +115,22 @@ internal void Free(MemoryAllocation allocation) // Even if they are free, some regions cannot be allocated. // The only way to know if a region can be allocated is to try to allocate it. - try - { - _osService.AllocateMemory(_processHandle, finalRange.Start, (int)finalRange.GetSize(), - MemoryAllocationType.Commit | MemoryAllocationType.Reserve, - forExecutableCode ? MemoryProtection.ExecuteReadWrite : MemoryProtection.ReadWrite); + var allocateResult = _osService.AllocateMemory(_processHandle, finalRange.Start, + (int)finalRange.GetSize(), MemoryAllocationType.Commit | MemoryAllocationType.Reserve, + forExecutableCode ? MemoryProtection.ExecuteReadWrite : MemoryProtection.ReadWrite); + + // If the allocation succeeded, return the range. + if (allocateResult.IsSuccess) return finalRange; - } - catch - { - // The allocation failed. Reset the current range and keep iterating. - freeRange = null; - continue; - } + + // The allocation failed. Reset the current range and keep iterating. + freeRange = null; + continue; } } - // If we reached the end of the memory range and didn't find a suitable free range, return null - return null; + // If we reached the end of the memory range and didn't find a suitable free range. + return new AllocationFailureOnNoFreeMemoryFound(actualRange.Value, nextAddress); } #region Store @@ -138,13 +141,37 @@ internal void Free(MemoryAllocation allocation) /// /// Size of the memory range to reserve. /// Set to true if the memory range must be executable. - /// The resulting reservation. - private MemoryReservation FindOrMakeReservation(ulong size, bool requireExecutable) + /// A result holding either the resulting reservation, or an allocation failure. + private Result FindOrMakeReservation(ulong size, bool requireExecutable) { uint alignment = _is64Bits ? (uint)8 : 4; - return _allocations.Select(r => r.TryReserveRange(size, alignment)) - .FirstOrDefault(r => r != null) - ?? Allocate(size, requireExecutable).ReserveRange(size, alignment); + var reservationInExistingAllocation = _allocations + .Where(a => !requireExecutable || a.IsExecutable) + .Select(r => r.ReserveRange(size, alignment)) + .FirstOrDefault(r => r.IsSuccess) + ?.Value; + + // Reservation successful in existing allocation + if (reservationInExistingAllocation != null) + return reservationInExistingAllocation; + + // No allocation could satisfy the reservation: allocate a new range + var allocationResult = Allocate(size, requireExecutable); + if (allocationResult.IsFailure) + return allocationResult.Error; + + // Make a reservation within that new allocation + var newAllocation = allocationResult.Value; + var reservationResult = newAllocation.ReserveRange(size, alignment); + if (reservationResult.IsFailure) + { + // There is no reason for the reservation to fail here, as we just allocated memory of sufficient size. + // Just in case, we free the memory and return the most appropriate failure. + Free(allocationResult.Value); + return new AllocationFailureOnNoFreeMemoryFound(newAllocation.Range, newAllocation.Range.Start); + } + + return reservationResult.Value; } /// @@ -153,24 +180,35 @@ private MemoryReservation FindOrMakeReservation(ulong size, bool requireExecutab /// /// Data to store. /// Set to true if the data is executable code. Defaults to false. - /// The reserved memory range. - public MemoryReservation Store(byte[] data, bool isCode = false) + /// A result holding either the reserved memory range, or an allocation failure. + public Result Store(byte[] data, bool isCode = false) { - var reservedRange = FindOrMakeReservation((ulong)data.Length, isCode); + var reservedRangeResult = FindOrMakeReservation((ulong)data.Length, isCode); + if (reservedRangeResult.IsFailure) + return reservedRangeResult.Error; + + var reservedRange = reservedRangeResult.Value; WriteBytes(reservedRange.Range.Start, data, MemoryProtectionStrategy.Ignore); return reservedRange; } /// /// Stores the given data in the specified allocated range. Returns the reservation that holds the data. + /// In most situations, you should use the or signatures + /// instead, to have the instance handle allocations automatically, unless you need to + /// manage them manually. /// /// Data to store. /// Allocated memory to store the data. - /// The reservation holding the data. - public MemoryReservation Store(byte[] data, MemoryAllocation allocation) + /// A result holding either the reservation storing the data, or a reservation failure. + public Result Store(byte[] data, MemoryAllocation allocation) { uint alignment = _is64Bits ? (uint)8 : 4; - var reservedRange = allocation.ReserveRange((ulong)data.Length, alignment); + var reservedRangeResult = allocation.ReserveRange((ulong)data.Length, alignment); + if (reservedRangeResult.IsFailure) + return reservedRangeResult.Error; + + var reservedRange = reservedRangeResult.Value; WriteBytes(reservedRange.Range.Start, data, MemoryProtectionStrategy.Ignore); return reservedRange; } @@ -182,18 +220,21 @@ public MemoryReservation Store(byte[] data, MemoryAllocation allocation) /// Value or structure to store. /// Type of the value or structure. /// The reservation holding the data. - public MemoryReservation Store(T value) + public Result Store(T value) => Store(value.ToBytes(), false); /// /// Stores the given value or structure in the specified range of memory. Returns the reservation that holds the /// data. + /// In most situations, you should use the or signatures + /// instead, to have the instance handle allocations automatically, unless you need to + /// manage them manually. /// /// Value or structure to store. /// Range of memory to store the data in. /// Type of the value or structure. /// The reservation holding the data. - public MemoryReservation Store(T value, MemoryAllocation allocation) where T: struct + public Result Store(T value, MemoryAllocation allocation) where T: struct => Store(value.ToBytes(), allocation); #endregion diff --git a/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs b/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs index e7640b2..02ab750 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs @@ -1,5 +1,6 @@ using System.Text; using MindControl.Native; +using MindControl.Results; namespace MindControl; @@ -9,7 +10,7 @@ public partial class ProcessMemory /// /// Gets or sets the time to wait for the spawned thread to return when injecting a library using the /// method. - /// By default, this value will be set to 10 seconds. + /// The default value is 10 seconds. /// public TimeSpan LibraryInjectionThreadTimeout { get; set; } = TimeSpan.FromSeconds(10); @@ -17,76 +18,62 @@ public partial class ProcessMemory /// Injects a library into the attached process. /// /// Path to the library file to inject into the process. - /// - public void InjectLibrary(string libraryPath) + /// A successful result, or an injection failure detailing how the operation failed. + public Result InjectLibrary(string libraryPath) { if (!IsAttached) - throw new InvalidOperationException("Cannot inject a library into a process that is not attached."); + throw new InvalidOperationException(DetachedErrorMessage); + // Check if the library file exists string absoluteLibraryPath = Path.GetFullPath(libraryPath); if (!File.Exists(absoluteLibraryPath)) - throw new FileNotFoundException("The library file to inject was not found.", absoluteLibraryPath); - - var allocatedLibPathAddress = UIntPtr.Zero; - var threadHandle = IntPtr.Zero; - + return new InjectionFailureOnLibraryFileNotFound(absoluteLibraryPath); + // The goal here is to call the LoadLibrary function from inside the target process. // We need to pass the address of the library path string as a parameter to the function. // To do this, we first need to write the path of the library to load into the target process memory. - try - { - // Write the library path string into the process memory - var libraryPathBytes = Encoding.Unicode.GetBytes(absoluteLibraryPath); - allocatedLibPathAddress = _osService.AllocateMemory(_processHandle, libraryPathBytes.Length + 1, - MemoryAllocationType.Commit | MemoryAllocationType.Reserve, MemoryProtection.ReadWrite); - _osService.WriteProcessMemory(_processHandle, allocatedLibPathAddress, libraryPathBytes); - - // Create a thread that runs in the target process to run the LoadLibrary function, using the address of - // the library path string as a parameter, so that it knows to load that library. - var loadLibraryFunctionAddress = _osService.GetLoadLibraryFunctionAddress(); - threadHandle = _osService.CreateRemoteThread(_processHandle, loadLibraryFunctionAddress, - allocatedLibPathAddress); + // Write the library path string into the process memory + var libraryPathBytes = Encoding.Unicode.GetBytes(absoluteLibraryPath); + var allocateStringResult = _osService.AllocateMemory(_processHandle, libraryPathBytes.Length + 1, + MemoryAllocationType.Commit | MemoryAllocationType.Reserve, MemoryProtection.ReadWrite); + if (allocateStringResult.IsFailure) + return new InjectionFailureOnSystemFailure("Could not allocate memory to store the library file path.", + allocateStringResult.Error); + var allocatedLibPathAddress = allocateStringResult.Value; + + var writeStringResult = _osService.WriteProcessMemory(_processHandle, allocatedLibPathAddress, + libraryPathBytes); + if (writeStringResult.IsFailure) + return new InjectionFailureOnSystemFailure( + "Could not write the library file path to the target process memory.", + writeStringResult.Error); + + // Create a thread that runs in the target process to run the LoadLibrary function, using the address of + // the library path string as a parameter, so that it knows to load that library. + var loadLibraryAddressResult = _osService.GetLoadLibraryFunctionAddress(); + if (loadLibraryAddressResult.IsFailure) + return new InjectionFailureOnSystemFailure( + "Could not get the address of the LoadLibrary system API function from the current process.", + loadLibraryAddressResult.Error); - // Wait for the thread to finish - if (!_osService.WaitThread(threadHandle, LibraryInjectionThreadTimeout)) - throw new TimeoutException("The injection timed out."); - } - catch (Exception ex) - { - var exceptions = new List {ex}; - - // Free the memory used for the library path string - if (allocatedLibPathAddress != UIntPtr.Zero) - { - try - { - _osService.ReleaseMemory(_processHandle, allocatedLibPathAddress); - } - catch (Exception freeEx) - { - exceptions.Add(freeEx); - } - } + var loadLibraryFunctionAddress = loadLibraryAddressResult.Value; + var threadHandleResult = _osService.CreateRemoteThread(_processHandle, loadLibraryFunctionAddress, + allocatedLibPathAddress); + if (threadHandleResult.IsFailure) + return new InjectionFailureOnSystemFailure( + "Could not create a remote thread in the target process to load the library.", + threadHandleResult.Error); + + var threadHandle = threadHandleResult.Value; - // Close the thread handle - if (threadHandle != IntPtr.Zero) - { - try - { - _osService.CloseHandle(threadHandle); - } - catch (Exception closeEx) - { - exceptions.Add(closeEx); - } - } - - if (exceptions.Count == 1) - throw; - - throw new AggregateException("An error occurred while injecting the library into the process. Additional errors also occurred when trying to release resources.", exceptions); - } + // Wait for the thread to finish + var waitResult = _osService.WaitThread(threadHandle, LibraryInjectionThreadTimeout); + if (waitResult.IsFailure) + return new InjectionFailureOnSystemFailure("Could not wait for the thread to finish execution.", + waitResult.Error); + if (waitResult.Value == false) + return new InjectionFailureOnTimeout(); // Free the memory used for the library path string _osService.ReleaseMemory(_processHandle, allocatedLibPathAddress); @@ -98,8 +85,8 @@ public void InjectLibrary(string libraryPath) // We do this because we don't know if the LoadLibrary function succeeded or not. string expectedModuleName = Path.GetFileName(libraryPath); if (GetModuleAddress(expectedModuleName) == null) - { - throw new MemoryException("The module was not found in the process after the injection. The injection may have failed. Please check that your DLL is compatible with the target process (x64/x86)."); - } + return new InjectionFailureOnModuleNotFound(); + + return Result.Success; } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs b/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs index 6b954b1..d6a1693 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs @@ -1,5 +1,4 @@ -using System.Text.RegularExpressions; -using MindControl.Native; +using MindControl.Native; namespace MindControl; @@ -62,24 +61,20 @@ public partial class ProcessMemory /// to keep your program responsive while the scan is going on. /// Read the documentation to learn how to perform efficient scans. /// - /// String representation of the byte pattern to find. This pattern should be a series of - /// hexadecimal bytes, optionally separated by spaces. Each character, excluding spaces, can be a specific value - /// (0-F) or a wildcard "?" character, indicating that the value to look for at this position could be any value. - /// Read the documentation for more information. + /// Byte pattern to look for. See . You can use a + /// string instead and it will be converted implicitly. An example would be "1F ?? 4B 00 ?6". /// Range of memory to scan. Leave this to null (the default) to scan the whole process /// memory. Restricting the memory range can dramatically improve the performance of the scan. /// Settings for the search. Leave this to null (the default) to use the default settings. /// Using more restrictive settings can dramatically improve the performance of the scan. /// An enumerable of addresses where the pattern was found. - public IEnumerable FindBytes(string bytePattern, MemoryRange? range = null, + public IEnumerable FindBytes(ByteSearchPattern pattern, MemoryRange? range = null, FindBytesSettings? settings = null) { var actualSettings = settings ?? new FindBytesSettings(); var actualRange = GetClampedMemoryRange(range); actualSettings.Validate(); - - (byte[] bytePatternArray, byte[] maskArray) = ParseBytePattern(bytePattern); - return FindBytesInternal(bytePatternArray, maskArray, actualRange, actualSettings); + return FindBytesInternal(pattern, actualRange, actualSettings); } /// @@ -89,110 +84,20 @@ public IEnumerable FindBytes(string bytePattern, MemoryRange? range = n /// responsive while the scan is going on. /// Read the documentation to learn how to perform efficient scans. /// - /// String representation of the byte pattern to find. This pattern should be a series of - /// hexadecimal bytes, optionally separated by spaces. Each character, excluding spaces, can be a specific value - /// (0-F) or a wildcard "?" character, indicating that the value to look for at this position could be any value. - /// Read the documentation for more information. + /// Byte pattern to look for. See . You can use a + /// string instead and it will be converted implicitly. An example would be "1F ?? 4B 00 ?6". /// Range of memory to scan. Leave this to null (the default) to scan the whole process /// memory. Restricting the memory range can dramatically improve the performance of the scan. /// Settings for the search. Leave this to null (the default) to use the default settings. /// Using more restrictive settings can dramatically improve the performance of the scan. /// An asynchronous enumerable of addresses where the pattern was found. - public async IAsyncEnumerable FindBytesAsync(string bytePattern, MemoryRange? range = null, + public async IAsyncEnumerable FindBytesAsync(ByteSearchPattern pattern, MemoryRange? range = null, FindBytesSettings? settings = null) { - var results = await Task.Run(() => FindBytes(bytePattern, range, settings)); + var results = await Task.Run(() => FindBytes(pattern, range, settings)); foreach (var result in results) yield return result; } - - #region Parsing and input processing - - private readonly Regex _bytePatternRegex = new("^([0-9A-F?]{2})*$", - RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - - /// - /// Parses the byte pattern string into a byte array and a mask array. - /// These arrays are used to compare the pattern with the memory when scanning for matches. - /// - /// The byte pattern string to parse. - /// A tuple containing the byte array and the mask array. - /// Thrown if the byte pattern is invalid. - private Tuple ParseBytePattern(string bytePattern) - { - if (string.IsNullOrWhiteSpace(bytePattern)) - { - throw new ArgumentException("The byte pattern cannot be null or empty.", nameof(bytePattern)); - } - - var pattern = bytePattern.Replace(" ", ""); - if (pattern.Length % 2 != 0) - { - throw new ArgumentException("The byte pattern must contain an even number of non-space characters.", - nameof(bytePattern)); - } - if (!_bytePatternRegex.IsMatch(pattern)) - { - throw new ArgumentException("The byte pattern must contain only hexadecimal characters and '?' wildcards.", - nameof(bytePattern)); - } - if (pattern == new string('?', pattern.Length)) - { - throw new ArgumentException("The byte pattern cannot contain only '?' wildcards.", nameof(bytePattern)); - } - - var bytePatternArray = new byte[pattern.Length / 2]; - var maskArray = new byte[pattern.Length / 2]; - for (var i = 0; i < pattern.Length; i += 2) - { - var byteString = pattern.Substring(i, 2); - if (byteString[0] == '?' || byteString[1] == '?') - { - // Both bytes are unknown. Set both the value and the mask to 0. - bytePatternArray[i / 2] = 0; - maskArray[i / 2] = 0; - } - else if (byteString[0] == '?') - { - // The first byte is unknown. Set the value to the second byte and the mask to 0xF. - bytePatternArray[i / 2] = Convert.ToByte(byteString[1].ToString(), 16); - maskArray[i / 2] = 0xF; - } - else if (byteString[1] == '?') - { - // The second byte is unknown. Set the value to the first byte multiplied by 16 and the mask to 0xF0. - bytePatternArray[i / 2] = (byte)(Convert.ToByte(byteString[0].ToString(), 16) * 16); - maskArray[i / 2] = 0xF0; - } - else - { - // Both bytes are known. Set the value to the byte and the mask to 0xFF. - bytePatternArray[i / 2] = Convert.ToByte(byteString, 16); - maskArray[i / 2] = 0xFF; - } - } - - return new Tuple(bytePatternArray, maskArray); - } - - /// - /// Returns the intersection of the input range with the full addressable memory range. - /// If the input memory range is null, the full memory range is returned. - /// - /// Input memory range to clamp. If null, the full memory range is returned. - /// The clamped memory range, or the full memory range if the input is null. - private MemoryRange GetClampedMemoryRange(MemoryRange? input) - { - var fullMemoryRange = _osService.GetFullMemoryRange(); - return input == null ? fullMemoryRange : new MemoryRange( - Start: input.Value.Start.ToUInt64() < fullMemoryRange.Start.ToUInt64() - ? fullMemoryRange.Start : input.Value.Start, - End: input.Value.End.ToUInt64() > fullMemoryRange.End.ToUInt64() - ? fullMemoryRange.End : input.Value.End - ); - } - - #endregion #region Search implementation @@ -200,18 +105,17 @@ private MemoryRange GetClampedMemoryRange(MemoryRange? input) /// Scans the memory of the target process for a byte pattern. Returns the address of each occurrence in the /// target range. /// - /// Byte pattern to search for. - /// Mask to use when comparing the pattern with the memory. + /// Byte pattern to search for. /// Memory range to scan. /// Settings for the search. - private IEnumerable FindBytesInternal(byte[] bytePattern, byte[] mask, MemoryRange range, + private IEnumerable FindBytesInternal(ByteSearchPattern pattern, MemoryRange range, FindBytesSettings settings) { var regionRanges = GetAggregatedRegionRanges(range, settings); var resultCount = 0; foreach (var regionRange in regionRanges) { - foreach (var address in ScanRangeForBytePattern(bytePattern, mask, regionRange)) + foreach (var address in ScanRangeForBytePattern(pattern.ByteArray, pattern.Mask, regionRange)) { yield return address; @@ -233,7 +137,7 @@ private IEnumerable ScanRangeForBytePattern(byte[] bytePattern, byte[] { // Read the whole memory range and place it in a byte array. byte[] rangeMemory = _osService.ReadProcessMemory(_processHandle, range.Start, range.GetSize()) - ?? Array.Empty(); + .GetValueOrDefault() ?? Array.Empty(); int maxIndex = rangeMemory.Length - bytePattern.Length; @@ -278,7 +182,15 @@ private MemoryRange[] GetAggregatedRegionRanges(MemoryRange range, FindBytesSett UIntPtr currentAddress = range.Start; while (currentAddress.ToUInt64() <= rangeEnd) { - var currentRangeMetadata = _osService.GetRegionMetadata(_processHandle, currentAddress, _is64Bits); + var getRegionResult = _osService.GetRegionMetadata(_processHandle, currentAddress, _is64Bits); + if (getRegionResult.IsFailure) + { + // If we failed to get the region metadata, we cannot continue because we don't know where the next + // region starts. + break; + } + + var currentRangeMetadata = getRegionResult.Value; if (currentRangeMetadata.Size.ToUInt64() == 0) { // We cannot keep browsing if the size is 0 because we don't know where the next region starts. diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs index 334680a..ee0b638 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using MindControl.Internal; +using MindControl.Results; namespace MindControl; @@ -8,34 +9,46 @@ namespace MindControl; public partial class ProcessMemory { #region Public methods - + /// /// Reads a specific type of data from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. /// Type of data to read. Some types are not supported and will cause the method to throw /// an . Do not use Nullable types. - /// The value read from the process memory, or null if no value could be read. - public T? Read(PointerPath pointerPath) => (T?)Read(typeof(T), pointerPath); + /// The value read from the process memory, or a read failure. + public Result Read(PointerPath pointerPath) + { + var objectReadResult = Read(typeof(T), pointerPath); + return objectReadResult.IsSuccess ? (T)objectReadResult.Value : objectReadResult.Error; + } /// /// Reads a specific type of data from the given address, in the process memory. /// /// Target address in the process memory. /// Type of data to read. Some types are not supported and will cause the method to throw - /// an . Do not use Nullable types. - /// The value read from the process memory, or null if no value could be read. - public T? Read(UIntPtr address) => (T?)Read(typeof(T), address); + /// an . Do not use reference (nullable) types. + /// The value read from the process memory, or a read failure. + public Result Read(UIntPtr address) + { + var objectReadResult = Read(typeof(T), address); + return objectReadResult.IsSuccess ? (T)objectReadResult.Value : objectReadResult.Error; + } /// /// Reads a specific type of data from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. /// Type of data to read. Some types are not supported and will cause the method to throw - /// an . Do not use Nullable types. - /// The value read from the process memory, or null if no value could be read. - public object? Read(Type dataType, PointerPath pointerPath) - => Read(dataType, EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero); + /// an . Do not use reference (nullable) types. + /// The value read from the process memory, or a read failure. + public Result Read(Type dataType, PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? Read(dataType, addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads a specific type of data from the given address, in the process memory. @@ -43,38 +56,45 @@ public partial class ProcessMemory /// Target address in the process memory. /// Type of data to read. Some types are not supported and will cause the method to throw /// an . Do not use Nullable types. - /// The value read from the process memory, or null if no value could be read. - public object? Read(Type dataType, UIntPtr address) + /// The value read from the process memory, or a read failure. + public Result Read(Type dataType, UIntPtr address) { - if (dataType == typeof(bool)) return ReadBool(address); - if (dataType == typeof(byte)) return ReadByte(address); - if (dataType == typeof(short)) return ReadShort(address); - if (dataType == typeof(ushort)) return ReadUShort(address); - if (dataType == typeof(int)) return ReadInt(address); - if (dataType == typeof(uint)) return ReadUInt(address); - if (dataType == typeof(IntPtr)) return ReadIntPtr(address); - if (dataType == typeof(float)) return ReadFloat(address); - if (dataType == typeof(long)) return ReadLong(address); - if (dataType == typeof(ulong)) return ReadULong(address); - if (dataType == typeof(double)) return ReadDouble(address); + if (dataType == typeof(bool)) return Result.CastValueFrom(ReadBool(address)); + if (dataType == typeof(byte)) return Result.CastValueFrom(ReadByte(address)); + if (dataType == typeof(short)) return Result.CastValueFrom(ReadShort(address)); + if (dataType == typeof(ushort)) return Result.CastValueFrom(ReadUShort(address)); + if (dataType == typeof(int)) return Result.CastValueFrom(ReadInt(address)); + if (dataType == typeof(uint)) return Result.CastValueFrom(ReadUInt(address)); + if (dataType == typeof(IntPtr)) return Result.CastValueFrom(ReadIntPtr(address)); + if (dataType == typeof(float)) return Result.CastValueFrom(ReadFloat(address)); + if (dataType == typeof(long)) return Result.CastValueFrom(ReadLong(address)); + if (dataType == typeof(ulong)) return Result.CastValueFrom(ReadULong(address)); + if (dataType == typeof(double)) return Result.CastValueFrom(ReadDouble(address)); // Not a primitive type. Try to read it as a structure. // To do that, we first have to determine the size of the structure. int size = Marshal.SizeOf(dataType); // Then we read the bytes from the process memory. - var bytes = ReadBytes(address, (ulong)size); - if (bytes == null) - return null; + var bytesResult = ReadBytes(address, (ulong)size); + if (!bytesResult.IsSuccess) + return bytesResult; // We have the bytes. Now we need to convert them to the structure. // We cannot use MemoryMarshal.Read here because the data type is a variable, not a generic type. // So we have to use a GCHandle to pin the bytes in memory and then use Marshal.PtrToStructure. - var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + var handle = GCHandle.Alloc(bytesResult.Value, GCHandleType.Pinned); try { var pointer = handle.AddrOfPinnedObject(); - return Marshal.PtrToStructure(pointer, dataType); + object? structure = Marshal.PtrToStructure(pointer, dataType); + if (structure == null) + return new ReadFailureOnConversionFailure(); + return structure; + } + catch (Exception) + { + return new ReadFailureOnConversionFailure(); } finally { @@ -82,234 +102,265 @@ public partial class ProcessMemory handle.Free(); } } - + /// /// Reads a boolean from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. - /// The value read from the process memory, or null if no value could be read. - public bool? ReadBool(PointerPath pointerPath) => ReadBool(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero); + /// The value read from the process memory, or a read failure. + public Result ReadBool(PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadBool(addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads a boolean from the given address in the process memory. /// /// Target address in the process memory. - /// The value read from the process memory, or null if no value could be read. - public bool? ReadBool(UIntPtr address) + /// The value read from the process memory, or a read failure. + public Result ReadBool(UIntPtr address) { - var bytes = ReadBytes(address, 1); - if (bytes == null) - return null; - - return BitConverter.ToBoolean(bytes); + var bytesResult = ReadBytes(address, 1); + if (bytesResult.IsFailure) + return bytesResult.Error; + return BitConverter.ToBoolean(bytesResult.Value); } - + /// /// Reads a byte from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. - /// The value read from the process memory, or null if no value could be read. - public byte? ReadByte(PointerPath pointerPath) => ReadByte(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero); + /// The value read from the process memory, or a read failure. + public Result ReadByte(PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadByte(addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads a byte from the given address in the process memory. /// /// Target address in the process memory. - /// The value read from the process memory, or null if no value could be read. - public byte? ReadByte(UIntPtr address) + /// The value read from the process memory, or a read failure. + public Result ReadByte(UIntPtr address) { - var bytes = ReadBytes(address, 1); - return bytes?[0]; + var bytesResult = ReadBytes(address, 1); + return bytesResult.IsSuccess ? bytesResult.Value[0] : bytesResult.Error; } - + /// /// Reads a short from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. - /// The value read from the process memory, or null if no value could be read. - public short? ReadShort(PointerPath pointerPath) => ReadShort(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero); + /// The value read from the process memory, or a read failure. + public Result ReadShort(PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadShort(addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads a short from the given address in the process memory. /// /// Target address in the process memory. - /// The value read from the process memory, or null if no value could be read. - public short? ReadShort(UIntPtr address) + /// The value read from the process memory, or a read failure. + public Result ReadShort(UIntPtr address) { - var bytes = ReadBytes(address, 2); - if (bytes == null) - return null; - - return BitConverter.ToInt16(bytes); + var bytesResult = ReadBytes(address, 2); + return bytesResult.IsSuccess ? BitConverter.ToInt16(bytesResult.Value) : bytesResult.Error; } - + /// /// Reads an unsigned short from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. - /// The value read from the process memory, or null if no value could be read. - public ushort? ReadUShort(PointerPath pointerPath) => ReadUShort(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero); + /// The value read from the process memory, or a read failure. + public Result ReadUShort(PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadUShort(addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads an unsigned short from the given address in the process memory. /// /// Target address in the process memory. - /// The value read from the process memory, or null if no value could be read. - public ushort? ReadUShort(UIntPtr address) + /// The value read from the process memory, or a read failure. + public Result ReadUShort(UIntPtr address) { - var bytes = ReadBytes(address, 2); - if (bytes == null) - return null; - - return BitConverter.ToUInt16(bytes); + var bytesResult = ReadBytes(address, 2); + return bytesResult.IsSuccess ? BitConverter.ToUInt16(bytesResult.Value) : bytesResult.Error; } - + /// /// Reads an integer from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. - /// The value read from the process memory, or null if no value could be read. - public int? ReadInt(PointerPath pointerPath) => ReadInt(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero); + /// The value read from the process memory, or a read failure. + public Result ReadInt(PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadInt(addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads an integer from the given address in the process memory. /// /// Target address in the process memory. - /// The value read from the process memory, or null if no value could be read. - public int? ReadInt(UIntPtr address) + /// The value read from the process memory, or a read failure. + public Result ReadInt(UIntPtr address) { - var bytes = ReadBytes(address, 4); - if (bytes == null) - return null; - - return BitConverter.ToInt32(bytes); + var bytesResult = ReadBytes(address, 4); + return bytesResult.IsSuccess ? BitConverter.ToInt32(bytesResult.Value) : bytesResult.Error; } /// /// Reads an unsigned integer from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. - /// The value read from the process memory, or null if no value could be read. - public uint? ReadUInt(PointerPath pointerPath) => ReadUInt(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero); + /// The value read from the process memory, or a read failure. + public Result ReadUInt(PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadUInt(addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads an unsigned integer from the given address in the process memory. /// /// Target address in the process memory. - /// The value read from the process memory, or null if no value could be read. - public uint? ReadUInt(UIntPtr address) + /// The value read from the process memory, or a read failure. + public Result ReadUInt(UIntPtr address) { - var bytes = ReadBytes(address, 4); - if (bytes == null) - return null; - - return BitConverter.ToUInt32(bytes); + var bytesResult = ReadBytes(address, 4); + return bytesResult.IsSuccess ? BitConverter.ToUInt32(bytesResult.Value) : bytesResult.Error; } /// /// Reads a pointer from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. - /// The value read from the process memory, or null if no value could be read. - public UIntPtr? ReadIntPtr(PointerPath pointerPath) => ReadIntPtr(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero); + /// The value read from the process memory, or a read failure. + public Result ReadIntPtr(PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadIntPtr(addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads a pointer from the given address in the process memory. /// /// Target address in the process memory. - /// The value read from the process memory, or null if no value could be read. - public UIntPtr? ReadIntPtr(UIntPtr address) + /// The value read from the process memory, or a read failure. + public Result ReadIntPtr(UIntPtr address) { - var bytes = ReadBytes(address, (ulong)IntPtr.Size); - if (bytes == null) - return null; - return _is64Bits ? - (UIntPtr)BitConverter.ToUInt64(bytes) + var bytesResult = ReadBytes(address, (ulong)IntPtr.Size); + if (bytesResult.IsFailure) + return bytesResult.Error; + + byte[] bytes = bytesResult.Value; + return _is64Bits ? (UIntPtr)BitConverter.ToUInt64(bytes) : (UIntPtr)BitConverter.ToUInt32(bytes); } - + /// /// Reads a float from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. - /// The value read from the process memory, or null if no value could be read. - public float? ReadFloat(PointerPath pointerPath) => ReadFloat(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero); + /// The value read from the process memory, or a read failure. + public Result ReadFloat(PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadFloat(addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads a float from the given address in the process memory. /// /// Target address in the process memory. - /// The value read from the process memory, or null if no value could be read. - public float? ReadFloat(UIntPtr address) + /// The value read from the process memory, or a read failure. + public Result ReadFloat(UIntPtr address) { - var bytes = ReadBytes(address, 4); - if (bytes == null) - return null; - - return BitConverter.ToSingle(bytes); + var bytesResult = ReadBytes(address, 4); + return bytesResult.IsSuccess ? BitConverter.ToSingle(bytesResult.Value) : bytesResult.Error; } - + /// /// Reads a long from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. - /// The value read from the process memory, or null if no value could be read. - public long? ReadLong(PointerPath pointerPath) => ReadLong(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero); + /// The value read from the process memory, or a read failure. + public Result ReadLong(PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadLong(addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads a long from the given address in the process memory. /// /// Target address in the process memory. - /// The value read from the process memory, or null if no value could be read. - public long? ReadLong(UIntPtr address) + /// The value read from the process memory, or a read failure. + public Result ReadLong(UIntPtr address) { - var bytes = ReadBytes(address, 8); - if (bytes == null) - return null; - - return BitConverter.ToInt64(bytes); + var bytesResult = ReadBytes(address, 8); + return bytesResult.IsSuccess ? BitConverter.ToInt64(bytesResult.Value) : bytesResult.Error; } - + /// /// Reads an unsigned long from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. - /// The value read from the process memory, or null if no value could be read. - public ulong? ReadULong(PointerPath pointerPath) => ReadULong(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero); + /// The value read from the process memory, or a read failure. + public Result ReadULong(PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadULong(addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads an unsigned long from the given address in the process memory. /// /// Target address in the process memory. - /// The value read from the process memory, or null if no value could be read. - public ulong? ReadULong(UIntPtr address) + /// The value read from the process memory, or a read failure. + public Result ReadULong(UIntPtr address) { - var bytes = ReadBytes(address, 8); - if (bytes == null) - return null; - - return BitConverter.ToUInt64(bytes); + var bytesResult = ReadBytes(address, 8); + return bytesResult.IsSuccess ? BitConverter.ToUInt64(bytesResult.Value) : bytesResult.Error; } - + /// /// Reads a double from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. - /// The value read from the process memory, or null if no value could be read. - public double? ReadDouble(PointerPath pointerPath) => ReadDouble(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero); + /// The value read from the process memory, or a read failure. + public Result ReadDouble(PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadDouble(addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads a double from the given address in the process memory. /// /// Target address in the process memory. - /// The value read from the process memory, or null if no value could be read. - public double? ReadDouble(UIntPtr address) + /// The value read from the process memory, or a read failure. + public Result ReadDouble(UIntPtr address) { - var bytes = ReadBytes(address, 8); - if (bytes == null) - return null; - - return BitConverter.ToDouble(bytes); + var bytesResult = ReadBytes(address, 8); + return bytesResult.IsSuccess ? BitConverter.ToDouble(bytesResult.Value) : bytesResult.Error; } /// @@ -330,9 +381,14 @@ public partial class ProcessMemory /// you can try one of the presets (e.g. ), or try to figure out the details /// and provide your own string settings. It is recommended to at least use a preset, for performance and accuracy /// reasons. - /// The string read from memory, or null if any read operation fails. - public string? ReadString(PointerPath pointerPath, int maxSizeInBytes = 256, StringSettings? stringSettings = null) - => ReadString(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero, maxSizeInBytes, stringSettings); + /// The string read from memory, or a read failure. + public Result ReadString(PointerPath pointerPath, int maxSizeInBytes = 256, + StringSettings? stringSettings = null) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadString(addressResult.Value, maxSizeInBytes, stringSettings) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads a string from the given address in the process memory. @@ -352,18 +408,22 @@ public partial class ProcessMemory /// you can try one of the presets (e.g. ), or try to figure out the details /// and provide your own string settings. It is recommended to at least use a preset, for performance and accuracy /// reasons. - /// The string read from memory, or null if any read operation fails. - public string? ReadString(UIntPtr address, int maxSizeInBytes = 256, StringSettings? stringSettings = null) + /// The string read from memory, or a read failure. + public Result ReadString(UIntPtr address, int maxSizeInBytes = 256, + StringSettings? stringSettings = null) { var actualStringSettings = stringSettings ?? GuessStringSettings(); var lengthToRead = (ulong)maxSizeInBytes; if (actualStringSettings.PrefixSettings != null) { - var lengthPrefixBytes = ReadBytes(address, actualStringSettings.PrefixSettings.PrefixSize); - if (lengthPrefixBytes == null) - return null; + var lengthPrefixBytesResult = ReadBytes(address, actualStringSettings.PrefixSettings.PrefixSize); + + if (lengthPrefixBytesResult.IsFailure) + return lengthPrefixBytesResult.Error; + byte[] lengthPrefixBytes = lengthPrefixBytesResult.Value; + // Automatically determine the length unit if not provided: // Should be the minimal number of bytes supported by the encoding for a single character. // To get that, we make the encoding output the bytes for the string "a" and count the bytes. @@ -376,9 +436,10 @@ public partial class ProcessMemory } // Read the actual bytes on the full length - var bytes = ReadBytes(address, lengthToRead); - if (bytes == null) - return null; + var stringBytesResult = ReadBytes(address, lengthToRead); + if (!stringBytesResult.IsSuccess) + return stringBytesResult.Error; + byte[] bytes = stringBytesResult.Value; // Convert the whole byte array to a string string fullString = actualStringSettings.Encoding.GetString(bytes); @@ -392,37 +453,49 @@ public partial class ProcessMemory /// /// Optimized, reusable path to the target address. /// Number of bytes to read. - /// The value read from the process memory, or null if no value could be read. - public byte[]? ReadBytes(PointerPath pointerPath, long length) => ReadBytes(pointerPath, (ulong)length); - + /// The value read from the process memory, or a read failure. + public Result ReadBytes(PointerPath pointerPath, long length) + => ReadBytes(pointerPath, (ulong)length); + /// /// Reads a sequence of bytes from the address referred by the given pointer path, in the process memory. /// /// Optimized, reusable path to the target address. /// Number of bytes to read. - /// The value read from the process memory, or null if no value could be read. - public byte[]? ReadBytes(PointerPath pointerPath, ulong length) - => ReadBytes(EvaluateMemoryAddress(pointerPath) ?? UIntPtr.Zero, length); + /// The value read from the process memory, or a read failure. + public Result ReadBytes(PointerPath pointerPath, ulong length) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadBytes(addressResult.Value, length) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Reads a sequence of bytes from the given address in the process memory. /// /// Target address in the process memory. /// Number of bytes to read. - /// The value read from the process memory, or null if no value could be read. - public byte[]? ReadBytes(UIntPtr address, long length) => ReadBytes(address, (ulong)length); + /// The value read from the process memory, or a read failure. + public Result ReadBytes(UIntPtr address, long length) + => ReadBytes(address, (ulong)length); /// /// Reads a sequence of bytes from the given address in the process memory. /// /// Target address in the process memory. /// Number of bytes to read. - /// The value read from the process memory, or null if no value could be read. - public byte[]? ReadBytes(UIntPtr address, ulong length) + /// The value read from the process memory, or a read failure. + public Result ReadBytes(UIntPtr address, ulong length) { - if (address == UIntPtr.Zero || !IsBitnessCompatible(address)) - return null; - return _osService.ReadProcessMemory(_processHandle, address, length); + if (address == UIntPtr.Zero) + return new ReadFailureOnZeroPointer(); + + if (!IsBitnessCompatible(address)) + return new ReadFailureOnIncompatibleBitness(address); + + var readResult = _osService.ReadProcessMemory(_processHandle, address, length); + return readResult.IsSuccess ? readResult.Value + : new ReadFailureOnSystemRead(readResult.Error); } #endregion @@ -443,12 +516,12 @@ private StringSettings GuessStringSettings() // an attempt to simplify things for hacking beginners who might not be able to figure out what their string // settings should be. It is designed to only be called when the user doesn't provide a string setting. - string[] moduleNames = _process.Modules + var moduleNames = _process.Modules .Cast() .Select(m => m.ModuleName?.ToLowerInvariant()) .Where(m => m != null) .Cast() - .ToArray(); + .ToHashSet(); if (moduleNames.Contains("java.exe")) return StringSettings.Java; diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs index a303f15..56e84da 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs @@ -1,5 +1,6 @@ using MindControl.Internal; using MindControl.Native; +using MindControl.Results; namespace MindControl; @@ -15,8 +16,14 @@ public partial class ProcessMemory /// of this instance is used. /// Type of the value to write. /// Thrown when the type of the value is not supported. - public void Write(PointerPath path, T value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => Write(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result Write(PointerPath path, T value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? Write(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes a value to the given address in the process memory. @@ -27,29 +34,31 @@ public void Write(PointerPath path, T value, MemoryProtectionStrategy? memory /// of this instance is used. /// Type of the value to write. /// Thrown when the type of the value is not supported. - public void Write(UIntPtr address, T value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result Write(UIntPtr address, T value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) { if (value == null) throw new ArgumentNullException(nameof(value), "The value to write cannot be null."); - - switch (value) + + return value switch { - case bool v: WriteBool(address, v, memoryProtectionStrategy); break; - case byte v: WriteByte(address, v, memoryProtectionStrategy); break; - case short v: WriteShort(address, v, memoryProtectionStrategy); break; - case ushort v: WriteUShort(address, v, memoryProtectionStrategy); break; - case int v: WriteInt(address, v, memoryProtectionStrategy); break; - case uint v: WriteUInt(address, v, memoryProtectionStrategy); break; - case IntPtr v: WriteIntPtr(address, v, memoryProtectionStrategy); break; - case float v: WriteFloat(address, v, memoryProtectionStrategy); break; - case long v: WriteLong(address, v, memoryProtectionStrategy); break; - case ulong v: WriteULong(address, v, memoryProtectionStrategy); break; - case double v: WriteDouble(address, v, memoryProtectionStrategy); break; - case byte[] v: WriteBytes(address, v, memoryProtectionStrategy); break; - default: WriteBytes(address, value.ToBytes(), memoryProtectionStrategy); break; - } + bool v => WriteBool(address, v, memoryProtectionStrategy), + byte v => WriteByte(address, v, memoryProtectionStrategy), + short v => WriteShort(address, v, memoryProtectionStrategy), + ushort v => WriteUShort(address, v, memoryProtectionStrategy), + int v => WriteInt(address, v, memoryProtectionStrategy), + uint v => WriteUInt(address, v, memoryProtectionStrategy), + IntPtr v => WriteIntPtr(address, v, memoryProtectionStrategy), + float v => WriteFloat(address, v, memoryProtectionStrategy), + long v => WriteLong(address, v, memoryProtectionStrategy), + ulong v => WriteULong(address, v, memoryProtectionStrategy), + double v => WriteDouble(address, v, memoryProtectionStrategy), + byte[] v => WriteBytes(address, v, memoryProtectionStrategy), + _ => WriteBytes(address, value.ToBytes(), memoryProtectionStrategy) + }; } - + /// /// Writes a boolean value to the address referred by the given pointer path in the process memory. /// @@ -58,8 +67,14 @@ public void Write(UIntPtr address, T value, MemoryProtectionStrategy? memoryP /// as a byte with the value 0. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteBool(PointerPath path, bool value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteBool(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result WriteBool(PointerPath path, bool value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteBool(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes a boolean value to the given address in the process memory. @@ -69,9 +84,11 @@ public void WriteBool(PointerPath path, bool value, MemoryProtectionStrategy? me /// as a byte with the value 0. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteBool(UIntPtr address, bool value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result WriteBool(UIntPtr address, bool value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, new[] { (byte)(value ? 1 : 0) }, memoryProtectionStrategy); - + /// /// Writes a byte to the address referred by the given pointer path in the process memory. /// @@ -79,8 +96,14 @@ public void WriteBool(UIntPtr address, bool value, MemoryProtectionStrategy? mem /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteByte(PointerPath path, byte value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteByte(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result WriteByte(PointerPath path, byte value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteByte(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes a byte to the given address in the process memory. @@ -89,9 +112,11 @@ public void WriteByte(PointerPath path, byte value, MemoryProtectionStrategy? me /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteByte(UIntPtr address, byte value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result WriteByte(UIntPtr address, byte value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, new[] { value }, memoryProtectionStrategy); - + /// /// Writes a short to the address referred by the given pointer path in the process memory. /// @@ -99,8 +124,14 @@ public void WriteByte(UIntPtr address, byte value, MemoryProtectionStrategy? mem /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteShort(PointerPath path, short value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteShort(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result WriteShort(PointerPath path, short value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteShort(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes a short to the given address in the process memory. @@ -109,9 +140,11 @@ public void WriteShort(PointerPath path, short value, MemoryProtectionStrategy? /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteShort(UIntPtr address, short value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result WriteShort(UIntPtr address, short value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - + /// /// Writes an unsigned short to the address referred by the given pointer path in the process memory. /// @@ -119,8 +152,14 @@ public void WriteShort(UIntPtr address, short value, MemoryProtectionStrategy? m /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteUShort(PointerPath path, ushort value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteUShort(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result WriteUShort(PointerPath path, ushort value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteUShort(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes an unsigned short to the given address in the process memory. @@ -129,7 +168,9 @@ public void WriteUShort(PointerPath path, ushort value, MemoryProtectionStrategy /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteUShort(UIntPtr address, ushort value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result WriteUShort(UIntPtr address, ushort value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); /// @@ -139,8 +180,14 @@ public void WriteUShort(UIntPtr address, ushort value, MemoryProtectionStrategy? /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteInt(PointerPath path, int value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteInt(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result WriteInt(PointerPath path, int value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteInt(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes an integer to the given address in the process memory. @@ -149,9 +196,11 @@ public void WriteInt(PointerPath path, int value, MemoryProtectionStrategy? memo /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteInt(UIntPtr address, int value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result WriteInt(UIntPtr address, int value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - + /// /// Writes an unsigned integer to the address referred by the given pointer path in the process memory. /// @@ -159,8 +208,14 @@ public void WriteInt(UIntPtr address, int value, MemoryProtectionStrategy? memor /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteUInt(PointerPath path, uint value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteUInt(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result WriteUInt(PointerPath path, uint value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteUInt(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes an unsigned integer to the given address in the process memory. @@ -169,9 +224,11 @@ public void WriteUInt(PointerPath path, uint value, MemoryProtectionStrategy? me /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteUInt(UIntPtr address, uint value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result WriteUInt(UIntPtr address, uint value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - + /// /// Writes a pointer to the address referred by the given pointer path in the process memory. /// @@ -179,8 +236,14 @@ public void WriteUInt(UIntPtr address, uint value, MemoryProtectionStrategy? mem /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteIntPtr(PointerPath path, IntPtr value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteIntPtr(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result WriteIntPtr(PointerPath path, IntPtr value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteIntPtr(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes a pointer to the given address in the process memory. @@ -189,9 +252,11 @@ public void WriteIntPtr(PointerPath path, IntPtr value, MemoryProtectionStrategy /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteIntPtr(UIntPtr address, IntPtr value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result WriteIntPtr(UIntPtr address, IntPtr value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, value.ToBytes(_is64Bits), memoryProtectionStrategy); - + /// /// Writes a float to the address referred by the given pointer path in the process memory. /// @@ -199,8 +264,14 @@ public void WriteIntPtr(UIntPtr address, IntPtr value, MemoryProtectionStrategy? /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteFloat(PointerPath path, float value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteFloat(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result WriteFloat(PointerPath path, float value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteFloat(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes a float to the given address in the process memory. @@ -209,9 +280,11 @@ public void WriteFloat(PointerPath path, float value, MemoryProtectionStrategy? /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteFloat(UIntPtr address, float value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result WriteFloat(UIntPtr address, float value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - + /// /// Writes a long to the address referred by the given pointer path in the process memory. /// @@ -219,8 +292,14 @@ public void WriteFloat(UIntPtr address, float value, MemoryProtectionStrategy? m /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteLong(PointerPath path, long value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteLong(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result WriteLong(PointerPath path, long value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteLong(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes a long to the given address in the process memory. @@ -229,9 +308,11 @@ public void WriteLong(PointerPath path, long value, MemoryProtectionStrategy? me /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteLong(UIntPtr address, long value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result WriteLong(UIntPtr address, long value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - + /// /// Writes an unsigned long to the address referred by the given pointer path in the process memory. /// @@ -239,8 +320,14 @@ public void WriteLong(UIntPtr address, long value, MemoryProtectionStrategy? mem /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteULong(PointerPath path, ulong value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteULong(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result WriteULong(PointerPath path, ulong value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteULong(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes an unsigned long to the given address in the process memory. @@ -249,9 +336,11 @@ public void WriteULong(PointerPath path, ulong value, MemoryProtectionStrategy? /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteULong(UIntPtr address, ulong value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result WriteULong(UIntPtr address, ulong value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - + /// /// Writes a double to the address referred by the given pointer path in the process memory. /// @@ -259,8 +348,14 @@ public void WriteULong(UIntPtr address, ulong value, MemoryProtectionStrategy? m /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteDouble(PointerPath path, double value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteDouble(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result WriteDouble(PointerPath path, double value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteDouble(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes a double to the given address in the process memory. @@ -269,9 +364,11 @@ public void WriteDouble(PointerPath path, double value, MemoryProtectionStrategy /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteDouble(UIntPtr address, double value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result WriteDouble(UIntPtr address, double value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - + /// /// Writes a sequence of bytes to the address referred by the given pointer path in the process memory. /// @@ -279,8 +376,14 @@ public void WriteDouble(UIntPtr address, double value, MemoryProtectionStrategy? /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteBytes(PointerPath path, byte[] value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteBytes(EvaluateMemoryAddressOrThrow(path), value, memoryProtectionStrategy); + /// A successful result, or a write failure + public Result WriteBytes(PointerPath path, byte[] value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteBytes(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } /// /// Writes a sequence of bytes to the given address in the process memory. @@ -289,25 +392,40 @@ public void WriteBytes(PointerPath path, byte[] value, MemoryProtectionStrategy? /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - public void WriteBytes(UIntPtr address, byte[] value, MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A successful result, or a write failure + public Result WriteBytes(UIntPtr address, byte[] value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) { // Remove protection if needed memoryProtectionStrategy ??= DefaultWriteStrategy; MemoryProtection? previousProtection = null; if (memoryProtectionStrategy is MemoryProtectionStrategy.Remove or MemoryProtectionStrategy.RemoveAndRestore) { - previousProtection = _osService.ReadAndOverwriteProtection( - _processHandle, _is64Bits, address, MemoryProtection.ExecuteReadWrite); + var protectionRemovalResult = _osService.ReadAndOverwriteProtection(_processHandle, _is64Bits, + address, MemoryProtection.ExecuteReadWrite); + + if (protectionRemovalResult.IsFailure) + return new WriteFailureOnSystemProtectionRemoval(address, protectionRemovalResult.Error); + + previousProtection = protectionRemovalResult.Value; } // Write memory - _osService.WriteProcessMemory(_processHandle, address, value); + var writeResult = _osService.WriteProcessMemory(_processHandle, address, value); + if (writeResult.IsFailure) + return new WriteFailureOnSystemWrite(address, writeResult.Error); // Restore protection if needed if (memoryProtectionStrategy == MemoryProtectionStrategy.RemoveAndRestore && previousProtection != MemoryProtection.ExecuteReadWrite) { - _osService.ReadAndOverwriteProtection(_processHandle, _is64Bits, address, previousProtection!.Value); + var protectionRestorationResult = _osService.ReadAndOverwriteProtection(_processHandle, _is64Bits, + address, previousProtection!.Value); + + if (protectionRestorationResult.IsFailure) + return new WriteFailureOnSystemProtectionRestoration(address, protectionRestorationResult.Error); } + + return Result.Success; } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.cs b/src/MindControl/ProcessMemory/ProcessMemory.cs index a33abac..c46aa5e 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.cs @@ -12,6 +12,10 @@ namespace MindControl; /// public partial class ProcessMemory : IDisposable { + /// Exception message to use in exceptions thrown when trying to use a detached process. + private const string DetachedErrorMessage = + "The process is not attached. It may have exited or this instance may have been disposed."; + private readonly Process _process; private readonly IOperatingSystemService _osService; private IntPtr _processHandle; @@ -126,14 +130,16 @@ private void Attach() { try { - _is64Bits = _osService.IsProcess64Bits(_process.Id); + _is64Bits = _osService.IsProcess64Bits(_process.Id).GetValueOrDefault(defaultValue: true); if (_is64Bits && !Environment.Is64BitOperatingSystem) throw new ProcessException(_process.Id, "A 32-bit program cannot attach to a 64-bit process."); _process.EnableRaisingEvents = true; _process.Exited += OnProcessExited; - _processHandle = _osService.OpenProcess(_process.Id); + var openResult = _osService.OpenProcess(_process.Id); + openResult.ThrowOnError(); + _processHandle = openResult.Value; IsAttached = true; } diff --git a/src/MindControl/Result/McResult.cs b/src/MindControl/Result/McResult.cs deleted file mode 100644 index cf2c70b..0000000 --- a/src/MindControl/Result/McResult.cs +++ /dev/null @@ -1,93 +0,0 @@ -namespace MindControl; - -/// -/// Represents the result of an operation that can either succeed or fail. -/// -/// Type of the error that can be returned in case of failure. -public class McResult -{ - /// - /// Gets the error that caused the operation to fail, if any. - /// - public TError? Error { get; } - - /// - /// Gets the error message that describes the error that caused the operation to fail, if any. - /// - public string? ErrorMessage { get; } - - /// - /// Gets a boolean indicating if the operation was successful. - /// - public bool IsSuccess { get; } - - /// - /// Initializes a new successful instance. - /// - protected McResult() { IsSuccess = true; } - - /// - /// Initializes a new failed instance. - /// - /// Error that caused the operation to fail. - /// Error message that describes the error that caused the operation to fail. - protected McResult(TError error, string? errorMessage) - { - Error = error; - ErrorMessage = errorMessage; - IsSuccess = false; - } - - /// - /// Creates a new successful instance. - /// - public static McResult Success() => new(); - - /// - /// Creates a new failed instance. - /// - /// Error that caused the operation to fail. - /// Error message that describes the error that caused the operation to fail. - public static McResult Failure(TError error, string? errorMessage = null) => new(error, errorMessage); -} - -/// -/// Represents the result of an operation that can either succeed or fail, with a result value in case of success. -/// -/// Type of the result that can be returned in case of success. -/// Type of the error that can be returned in case of failure. -public sealed class McResult : McResult -{ - /// - /// Gets the result of the operation, if any. - /// - public TResult? Result { get; } - - /// - /// Initializes a new successful instance. - /// - /// Result of the operation. - private McResult(TResult result) { Result = result; } - - /// - /// Initializes a new failed instance. - /// - /// Error that caused the operation to fail. - /// Error message that describes the error that caused the operation to fail. - private McResult(TError error, string? errorMessage) : base(error, errorMessage) { } - - /// - /// Creates a new successful instance. - /// - /// Result of the operation. - public static McResult Success(TResult result) - => new(result); - - /// - /// Creates a new failed instance. - /// - /// Error that caused the operation to fail. - /// Error message that describes the error that caused the operation to fail. - public new static McResult Failure(TError error, string? errorMessage = null) - => new(error, errorMessage); -} diff --git a/src/MindControl/Results/AllocationFailure.cs b/src/MindControl/Results/AllocationFailure.cs new file mode 100644 index 0000000..81cd6d4 --- /dev/null +++ b/src/MindControl/Results/AllocationFailure.cs @@ -0,0 +1,53 @@ +namespace MindControl.Results; + +/// +/// Represents a reason for a memory allocation operation to fail. +/// +public enum AllocationFailureReason +{ + /// + /// The provided limit range is not within the bounds of the target process applicative memory range. + /// + LimitRangeOutOfBounds, + + /// + /// No free memory was found in the target process that would be large enough to accomodate the specified size + /// within the searched range. + /// + NoFreeMemoryFound +} + +/// +/// Represents a failure in a memory allocation operation. +/// +/// Reason for the failure. +public abstract record AllocationFailure(AllocationFailureReason Reason); + +/// +/// Represents a failure in a memory allocation operation when the provided limit range is not within the bounds of the +/// target process applicative memory range. +/// +/// Applicative memory range of the target process. +public record AllocationFailureOnLimitRangeOutOfBounds(MemoryRange ApplicativeMemoryRange) + : AllocationFailure(AllocationFailureReason.LimitRangeOutOfBounds) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The provided limit range is not within the bounds of the target process applicative memory range ({ApplicativeMemoryRange})."; +} + +/// +/// Represents a failure in a memory allocation operation when no free memory large enough to accomodate the specified +/// size was found in the target process within the searched range. +/// +/// Searched range in the target process. +/// Last memory region address searched in the target process. +public record AllocationFailureOnNoFreeMemoryFound(MemoryRange SearchedRange, UIntPtr LastRegionAddressSearched) + : AllocationFailure(AllocationFailureReason.NoFreeMemoryFound) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"No free memory range large enough to accomodate the specified size was found in the target process within the searched range ({SearchedRange})."; +} diff --git a/src/MindControl/Results/FindBytesFailure.cs b/src/MindControl/Results/FindBytesFailure.cs new file mode 100644 index 0000000..b7bfc57 --- /dev/null +++ b/src/MindControl/Results/FindBytesFailure.cs @@ -0,0 +1,13 @@ +namespace MindControl.Results; + +/// +/// Represents a failure occurring when the provided byte search pattern is invalid. +/// +/// Message that explains what makes the pattern invalid. +public record InvalidBytePatternFailure(string Message) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The provided byte search pattern is invalid: {Message}"; +} diff --git a/src/MindControl/Results/InjectionFailure.cs b/src/MindControl/Results/InjectionFailure.cs new file mode 100644 index 0000000..0eb030a --- /dev/null +++ b/src/MindControl/Results/InjectionFailure.cs @@ -0,0 +1,84 @@ +namespace MindControl.Results; + +/// +/// Represents a reason for an injection operation to fail. +/// +public enum InjectionFailureReason +{ + /// + /// The library file to inject was not found. + /// + LibraryFileNotFound, + + /// + /// Failure when calling a system API function. + /// + SystemFailure, + + /// + /// The injection timed out. + /// + Timeout, + + /// + /// The library injection completed but the module cannot be found in the target process. + /// + ModuleNotFound +} + +/// +/// Represents a failure in an injection operation. +/// +public abstract record InjectionFailure(InjectionFailureReason Reason); + +/// +/// Represents a failure in an injection operation when the library file to inject was not found. +/// +/// Path to the library file that was not found. +public record InjectionFailureOnLibraryFileNotFound(string LibraryPath) + : InjectionFailure(InjectionFailureReason.LibraryFileNotFound) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The library file to inject was not found at \"{LibraryPath}\"."; +} + +/// +/// Represents a failure in an injection operation when calling a system API function. +/// +/// Message that explains the reason for the failure. +/// Details about the failure. +public record InjectionFailureOnSystemFailure(string Message, SystemFailure Details) + : InjectionFailure(InjectionFailureReason.SystemFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"Failed to inject the library due to a system API failure: {Details}"; +} + +/// +/// Represents a failure in an injection operation when the injection timed out. +/// +public record InjectionFailureOnTimeout() + : InjectionFailure(InjectionFailureReason.Timeout) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => "The injection timed out. The thread did not return in time."; +} + +/// +/// Represents a failure in an injection operation when the library injection completed but the module cannot be found +/// in the target process. +/// +public record InjectionFailureOnModuleNotFound() + : InjectionFailure(InjectionFailureReason.ModuleNotFound) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => "The library injection completed, but the module cannot be found in the target process. The injection thread has probably failed. Check that the library is valid and compatible with the target process (32-bit vs 64-bit)."; +} diff --git a/src/MindControl/Results/PathEvaluationFailure.cs b/src/MindControl/Results/PathEvaluationFailure.cs new file mode 100644 index 0000000..c83ec0b --- /dev/null +++ b/src/MindControl/Results/PathEvaluationFailure.cs @@ -0,0 +1,95 @@ +using System.Numerics; + +namespace MindControl.Results; + +/// +/// Represents a reason for a path evaluation operation to fail. +/// +public enum PathEvaluationFailureReason +{ + /// + /// The target process is 32-bits, but the path is not compatible with a 32-bit address space. + /// + IncompatibleBitness, + + /// + /// The module specified in the pointer path was not found. + /// + BaseModuleNotFound, + + /// + /// A pointer in the path is a zero pointer or otherwise out of the target process address space. + /// + PointerOutOfRange, + + /// + /// Failure when attempting to read a pointer from the path. + /// + PointerReadFailure +} + +/// +/// Represents a failure in a path evaluation operation. +/// +/// Reason for the failure. +public abstract record PathEvaluationFailure(PathEvaluationFailureReason Reason); + +/// +/// Represents a failure in a path evaluation operation when the target process is 32-bits, but the target memory +/// address is not within the 32-bit address space. +/// +/// Address where the value causing the issue was read. May be null if the first address +/// in the path caused the failure. +public record PathEvaluationFailureOnIncompatibleBitness(UIntPtr? PreviousAddress = null) + : PathEvaluationFailure(PathEvaluationFailureReason.IncompatibleBitness) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => "The specified pointer path contains 64-bit offsets, but the target process is 32-bits."; +} + +/// +/// Represents a failure in a path evaluation operation when the base module specified in the pointer path was not +/// found. +/// +/// Name of the module that was not found. +public record PathEvaluationFailureOnBaseModuleNotFound(string ModuleName) + : PathEvaluationFailure(PathEvaluationFailureReason.BaseModuleNotFound) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The module \"{ModuleName}\", referenced in the pointer path, was not found in the target process."; +} + +/// +/// Represents a failure in a path evaluation operation when a pointer in the path is out of the target process +/// address space. +/// +/// Address where the value causing the issue was read. May be null if the first address +/// in the path caused the failure. +/// Address that caused the failure. The address is a BigInteger because it may be beyond the +/// range of a UIntPtr. +public record PathEvaluationFailureOnPointerOutOfRange(UIntPtr? PreviousAddress, BigInteger Address) + : PathEvaluationFailure(PathEvaluationFailureReason.PointerOutOfRange) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The pointer path evaluated a pointer to an address that is out of the target process address space range: {Address}."; +} + +/// +/// Represents a failure in a path evaluation operation when invoking the system API to read an address. +/// +/// Address that caused the failure. +/// Details about the failure. +public record PathEvaluationFailureOnPointerReadFailure(UIntPtr Address, ReadFailure Failure) + : PathEvaluationFailure(PathEvaluationFailureReason.PointerReadFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"Failed to read a pointer at the address {Address}: {Failure}"; +} \ No newline at end of file diff --git a/src/MindControl/Results/ReadFailure.cs b/src/MindControl/Results/ReadFailure.cs new file mode 100644 index 0000000..473b4c8 --- /dev/null +++ b/src/MindControl/Results/ReadFailure.cs @@ -0,0 +1,103 @@ +namespace MindControl.Results; + +/// +/// Represents a reason for a memory read operation to fail. +/// +public enum ReadFailureReason +{ + /// + /// Failure when evaluating the pointer path to the target memory. + /// + PointerPathEvaluationFailure, + + /// + /// The target process is 32-bits, but the target memory address is not within the 32-bit address space. + /// + IncompatibleBitness, + + /// + /// The target pointer is a zero pointer. + /// + ZeroPointer, + + /// + /// Failure when invoking the system API to read the target memory. + /// + SystemReadFailure, + + /// + /// Failure when trying to convert the bytes read from memory to the target type. + /// + ConversionFailure +} + +/// +/// Represents a failure in a memory read operation. +/// +/// Reason for the failure. +public abstract record ReadFailure(ReadFailureReason Reason); + +/// +/// Represents a failure in a memory read operation when evaluating the pointer path to the target memory. +/// +/// Details about the failure. +public record ReadFailureOnPointerPathEvaluation(PathEvaluationFailure PathEvaluationFailure) + : ReadFailure(ReadFailureReason.PointerPathEvaluationFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"Failed to evaluate the specified pointer path: {PathEvaluationFailure}"; +} + +/// +/// Represents a failure in a memory read operation when the target process is 32-bits, but the target memory address is +/// not within the 32-bit address space. +/// +/// Address that caused the failure. +public record ReadFailureOnIncompatibleBitness(UIntPtr Address) + : ReadFailure(ReadFailureReason.IncompatibleBitness) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The address to read, {Address}, is a 64-bit address, but the target process is 32-bits."; +} + +/// +/// Represents a failure in a memory read operation when the target pointer is a zero pointer. +/// +public record ReadFailureOnZeroPointer() + : ReadFailure(ReadFailureReason.ZeroPointer) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => "The address to read is a zero pointer."; +} + +/// +/// Represents a failure in a memory read operation when invoking the system API to read the target memory. +/// +/// Details about the failure. +public record ReadFailureOnSystemRead(SystemFailure SystemReadFailure) + : ReadFailure(ReadFailureReason.SystemReadFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"Failed to read at the target address: {SystemReadFailure}"; +} + +/// +/// Represents a failure in a memory read operation when trying to convert the bytes read from memory to the target +/// type. +/// +public record ReadFailureOnConversionFailure() + : ReadFailure(ReadFailureReason.ConversionFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => "Failed to convert the bytes read from memory to the target type. Try using primitive types or structure types."; +} \ No newline at end of file diff --git a/src/MindControl/Results/ReservationFailure.cs b/src/MindControl/Results/ReservationFailure.cs new file mode 100644 index 0000000..a344e57 --- /dev/null +++ b/src/MindControl/Results/ReservationFailure.cs @@ -0,0 +1,31 @@ +namespace MindControl.Results; + +/// +/// Represents a reason for a memory reservation operation to fail. +/// +public enum ReservationFailureReason +{ + /// + /// No space is available within the allocated memory range to reserve the specified size. + /// + NoSpaceAvailable +} + +/// +/// Represents a failure in a memory reservation operation. +/// +/// Reason for the failure. +public abstract record ReservationFailure(ReservationFailureReason Reason); + +/// +/// Represents a failure in a memory reservation operation when no space is available within the allocated memory range +/// to reserve the specified size. +/// +public record ReservationFailureOnNoSpaceAvailable() + : ReservationFailure(ReservationFailureReason.NoSpaceAvailable) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => "No space is available within the allocated memory range to reserve the specified size."; +} \ No newline at end of file diff --git a/src/MindControl/Results/Result.cs b/src/MindControl/Results/Result.cs new file mode 100644 index 0000000..a755fa5 --- /dev/null +++ b/src/MindControl/Results/Result.cs @@ -0,0 +1,160 @@ +namespace MindControl.Results; + +/// +/// Represents the result of an operation that can either succeed or fail. +/// +/// Type of the error that can be returned in case of failure. +public class Result +{ + private readonly TError? _error; + + /// Default string representing a success. + protected const string SuccessString = "Success"; + + /// Default string representing a failure. + protected const string FailureString = "An unspecified failure occurred"; + + /// + /// Gets a successful instance. + /// + public static readonly Result Success = new(); + + /// + /// Gets the error that caused the operation to fail. Throws if the operation was successful. + /// Use this after checking to ensure the operation was not + /// successful. + /// + public TError Error => IsFailure ? _error! + : throw new InvalidOperationException("Cannot access the error of a successful result."); + + /// + /// Gets a boolean indicating if the operation was successful. + /// + public bool IsSuccess { get; } + + /// + /// Gets a boolean indicating if the operation was a failure. + /// + public bool IsFailure => !IsSuccess; + + /// + /// Initializes a new successful instance. + /// + protected Result() { IsSuccess = true; } + + /// + /// Initializes a new failed instance. + /// + /// Error that caused the operation to fail. + protected Result(TError error) + { + _error = error; + IsSuccess = false; + } + + /// + /// Throws a if the operation was not successful. + /// + public void ThrowOnError() + { + if (IsFailure) + throw new ResultFailureException(Error); + } + + /// + /// Creates a new failed instance. + /// + /// Error that caused the operation to fail. + public static Result Failure(TError error) => new(error); + + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => IsSuccess ? SuccessString : Error!.ToString() ?? FailureString; + + /// + /// Implicitly converts a result value to a successful instance. + /// + /// Result value to convert. + public static implicit operator Result(TError result) => Failure(result); +} + +/// +/// Represents the result of an operation that can either succeed or fail, with a result value in case of success. +/// +/// Type of the result that can be returned in case of success. +/// Type of the error that can be returned in case of failure. +public sealed class Result : Result +{ + private readonly TResult? _value; + + /// + /// Gets the resulting value of the operation. Throws if the operation was not successful. + /// Use this after checking to ensure the operation was successful. + /// + public TResult Value => IsSuccess ? _value! + : throw new InvalidOperationException("Cannot access the value of an unsuccessful result.", + new ResultFailureException(Error)); + + /// + /// Initializes a new successful instance. + /// + /// Result of the operation. + private Result(TResult value) { _value = value; } + + /// + /// Initializes a new failed instance. + /// + /// Error that caused the operation to fail. + private Result(TError error) : base(error) { } + + /// + /// Creates a new successful instance. + /// + /// Result of the operation. + public static Result FromResult(TResult result) + => new(result); + + /// + /// Creates a new failed instance. + /// + /// Error that caused the operation to fail. + public new static Result Failure(TError error) => new(error); + + /// + /// Gets the resulting value of the operation, or the default value if the operation was not successful. + /// You can optionally provide a specific default value to return if the operation was not successful. + /// + /// Default value to return if the operation was not successful. If not specified, + /// defaults to the default value for the result type. + public TResult? GetValueOrDefault(TResult? defaultValue = default) => IsSuccess ? Value : defaultValue; + + /// + /// Builds a new instance from a successful result with a different type. + /// + /// Result to convert. + /// Type of the value of the result to convert. + /// A new instance with the value of the input result. + public static Result CastValueFrom(Result result) + where TOtherResult : TResult + => result.IsSuccess ? new Result(result.Value!) : result.Error; + + /// + /// Implicitly converts a result value to a successful instance. + /// + /// Result value to convert. + public static implicit operator Result(TResult result) + => FromResult(result); + + /// + /// Implicitly converts a result value to a successful instance. + /// + /// Result value to convert. + public static implicit operator Result(TError result) + => Failure(result); + + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => IsSuccess ? Value?.ToString() ?? SuccessString : Error?.ToString() ?? FailureString; +} diff --git a/src/MindControl/Results/ResultFailureException.cs b/src/MindControl/Results/ResultFailureException.cs new file mode 100644 index 0000000..b4523aa --- /dev/null +++ b/src/MindControl/Results/ResultFailureException.cs @@ -0,0 +1,32 @@ +namespace MindControl.Results; + +/// +/// Exception that may be thrown by failed . +/// +public class ResultFailureException : Exception +{ + /// Default error message. + private const string DefaultMessage = "The operation failed."; + + /// + /// Gets the error object describing the failure. + /// + public T Error { get; } + + /// + /// Builds a with the given details. + /// + /// Error object describing the failure. + public ResultFailureException(T error) : this(error, null) {} + + /// + /// Builds a with the given details. + /// + /// Error object describing the failure. + /// Exception that is the cause of this exception, if any. + public ResultFailureException(T error, Exception? innerException) + : base(error?.ToString() ?? DefaultMessage, innerException) + { + Error = error; + } +} \ No newline at end of file diff --git a/src/MindControl/Results/SystemFailure.cs b/src/MindControl/Results/SystemFailure.cs new file mode 100644 index 0000000..ceab3ba --- /dev/null +++ b/src/MindControl/Results/SystemFailure.cs @@ -0,0 +1,51 @@ +namespace MindControl.Results; + +/// +/// Represents a reason for a system operation to fail. +/// +public enum SystemFailureReason +{ + /// + /// The arguments provided to the system operation are invalid. + /// + ArgumentsInvalid, + + /// + /// The system API call failed. + /// + OperatingSystemCallFailed +} + +/// +/// Represents a failure in an operating system operation. +/// +/// Reason for the failure. +public abstract record SystemFailure(SystemFailureReason Reason); + +/// +/// Represents a failure in an operating system operation when the provided arguments are invalid. +/// +/// Name of the argument that caused the failure. +/// Message that describes how the argument fails to meet expectations. +public record SystemFailureOnInvalidArgument(string ArgumentName, string Message) + : SystemFailure(SystemFailureReason.ArgumentsInvalid) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The value provided for \"{ArgumentName}\" is invalid: {Message}"; +} + +/// +/// Represents a failure in a system API call. +/// +/// Numeric code that identifies the error. Typically provided by the operating system. +/// Message that describes the error. Typically provided by the operating system. +public record OperatingSystemCallFailure(int ErrorCode, string ErrorMessage) + : SystemFailure(SystemFailureReason.OperatingSystemCallFailed) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"A system API call failed with error code {ErrorCode}: {ErrorMessage}"; +} \ No newline at end of file diff --git a/src/MindControl/Results/WriteFailure.cs b/src/MindControl/Results/WriteFailure.cs new file mode 100644 index 0000000..03cfc48 --- /dev/null +++ b/src/MindControl/Results/WriteFailure.cs @@ -0,0 +1,131 @@ +namespace MindControl.Results; + +/// +/// Represents a reason for a memory write operation to fail. +/// +public enum WriteFailureReason +{ + /// + /// Failure when evaluating the pointer path to the target memory. + /// + PointerPathEvaluationFailure, + + /// + /// The target process is 32-bits, but the target memory address is not within the 32-bit address space. + /// + IncompatibleBitness, + + /// + /// The target address is a zero pointer. + /// + ZeroPointer, + + /// + /// Failure when invoking the system API to remove the protection properties of the target memory space. + /// + SystemProtectionRemovalFailure, + + /// + /// Failure when invoking the system API to restore the protection properties of the target memory space after + /// writing. + /// + SystemProtectionRestorationFailure, + + /// + /// Failure when invoking the system API to write bytes in memory. + /// + SystemWriteFailure, + + /// + /// Failure when trying to convert the value to write to an array of bytes to write in memory. + /// + ConversionFailure +} + +/// +/// Represents a failure in a memory write operation. +/// +/// Reason for the failure. +public abstract record WriteFailure(WriteFailureReason Reason); + +/// +/// Represents a failure in a memory write operation when evaluating the pointer path to the target memory. +/// +/// Details about the failure. +public record WriteFailureOnPointerPathEvaluation(PathEvaluationFailure PathEvaluationFailure) + : WriteFailure(WriteFailureReason.PointerPathEvaluationFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"Failed to evaluate the specified pointer path: {PathEvaluationFailure}"; +} + +/// +/// Represents a failure in a memory write operation when the target process is 32-bits, but the target memory address +/// is not within the 32-bit address space. +/// +/// Address that caused the failure. +public record WriteFailureOnIncompatibleBitness(UIntPtr Address) + : WriteFailure(WriteFailureReason.IncompatibleBitness) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The address to write, {Address}, is a 64-bit address, but the target process is 32-bits."; +} + +/// +/// Represents a failure in a memory write operation when the address to write is a zero pointer. +/// +public record WriteFailureOnZeroPointer() + : WriteFailure(WriteFailureReason.ZeroPointer) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => "The address to write is a zero pointer."; +} + +/// +/// Represents a failure in a memory write operation when the system API call to remove the protection properties of +/// the target memory space fails. +/// +/// Address where the operation failed. +/// Details about the failure. +public record WriteFailureOnSystemProtectionRemoval(UIntPtr Address, SystemFailure Failure) + : WriteFailure(WriteFailureReason.SystemProtectionRemovalFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"Failed to remove the protection of the memory at address {Address}: {Failure}.{Environment.NewLine}Change the memory protection strategy to {nameof(MemoryProtectionStrategy)}.{nameof(MemoryProtectionStrategy.Ignore)} to prevent memory protection removal. As protection removal is the first step when writing a value, it may simply be that the provided target address does not point to valid memory."; +} + +/// +/// Represents a failure in a memory write operation when the system API call to restore the protection properties of +/// the target memory space after writing fails. +/// +/// Address where the operation failed. +/// Details about the failure. +public record WriteFailureOnSystemProtectionRestoration(UIntPtr Address, SystemFailure Failure) + : WriteFailure(WriteFailureReason.SystemProtectionRestorationFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The value was written successfully, but the protection of the memory at address {Address} could not be restored to its original value: {Failure}.{Environment.NewLine}Change the memory protection strategy to {nameof(MemoryProtectionStrategy)}.{nameof(MemoryProtectionStrategy.Remove)} to prevent memory protection restoration."; +} + +/// +/// Represents a failure in a memory write operation when the system API call to write bytes in memory fails. +/// +/// Address where the write operation failed. +/// Details about the failure. +public record WriteFailureOnSystemWrite(UIntPtr Address, SystemFailure Failure) + : WriteFailure(WriteFailureReason.SystemWriteFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"Failed to write at the address {Address}: {Failure}"; +} diff --git a/src/MindControl/Search/ByteSearchPattern.cs b/src/MindControl/Search/ByteSearchPattern.cs new file mode 100644 index 0000000..5e59b52 --- /dev/null +++ b/src/MindControl/Search/ByteSearchPattern.cs @@ -0,0 +1,150 @@ +using System.Text.RegularExpressions; +using MindControl.Results; + +namespace MindControl; + +/// +/// Represents a pattern of bytes to search for in memory. +/// +public class ByteSearchPattern +{ + /// Regular expression pattern to match a valid byte pattern string. + private static readonly Regex BytePatternRegex = new("^([0-9A-F?]{2})*$", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + /// Original pattern string that was used to create this instance. + private readonly string _originalPatternString; + + /// + /// Gets the byte array part of the pattern. + /// + public byte[] ByteArray { get; } + + /// + /// Gets the byte mask part of the pattern. + /// + public byte[] Mask { get; } + + /// + /// Creates a new instance of from a pattern string. + /// Prefer for better error handling in cases where your pattern may be invalid. + /// + /// String representation of the byte pattern to find. This pattern should be a series + /// of hexadecimal bytes, optionally separated by spaces. Each character, excluding spaces, can be a specific value + /// (0-F) or a wildcard "?" character, indicating that the value to look for at this position could be any value. + /// An example would be "1F ?? 4B 00 ?6". Read the documentation for more information. + /// Thrown when the pattern string is invalid. + public ByteSearchPattern(string patternString) + { + var parseResult = ParsePatternString(patternString); + if (parseResult.IsFailure) + throw new ArgumentException(parseResult.Error.ToString(), nameof(patternString)); + + _originalPatternString = patternString; + (byte[] byteArray, byte[] mask) = parseResult.Value; + ByteArray = byteArray; + Mask = mask; + } + + /// + /// Creates a new instance of from a byte array and a mask array. + /// + /// Original pattern string that was parsed into byte arrays. + /// Byte array part of the pattern. + /// Byte mask part of the pattern. + protected ByteSearchPattern(string originalPatternString, byte[] byteArray, byte[] mask) + { + _originalPatternString = originalPatternString; + ByteArray = byteArray; + Mask = mask; + } + + /// + /// Attempts to parse a pattern string into a instance. + /// + /// String representation of the byte pattern to find. This pattern should be a series + /// of hexadecimal bytes, optionally separated by spaces. Each character, excluding spaces, can be a specific value + /// (0-F) or a wildcard "?" character, indicating that the value to look for at this position could be any value. + /// An example would be "1F ?? 4B 00 ?6". Read the documentation for more information. + /// A result holding either the parsed , or an instance of + /// detailing the reason for the failure. + public static Result TryParse(string patternString) + { + var parseResult = ParsePatternString(patternString); + if (parseResult.IsFailure) + return parseResult.Error; + + (byte[] byteArray, byte[] mask) = parseResult.Value; + return new ByteSearchPattern(patternString, byteArray, mask); + } + + /// + /// Parses a pattern string into a byte array and a mask array. + /// + /// Pattern string to parse. + /// A tuple containing the byte array and the mask array parsed from the pattern string, or an instance of + /// detailing the reason for the failure. + private static Result, InvalidBytePatternFailure> ParsePatternString( + string patternString) + { + if (string.IsNullOrWhiteSpace(patternString)) + return new InvalidBytePatternFailure("The pattern cannot be null or empty."); + + patternString = patternString.Replace(" ", ""); + if (patternString.Length % 2 != 0) + return new InvalidBytePatternFailure( + "The pattern must contain an even number of non-space characters."); + + if (!BytePatternRegex.IsMatch(patternString)) + return new InvalidBytePatternFailure( + "The pattern must contain only hexadecimal characters and '?' wildcards."); + + if (patternString == new string('?', patternString.Length)) + return new InvalidBytePatternFailure("The pattern cannot contain only '?' wildcards."); + + var bytePatternArray = new byte[patternString.Length / 2]; + var maskArray = new byte[patternString.Length / 2]; + for (var i = 0; i < patternString.Length; i += 2) + { + string byteString = patternString.Substring(i, 2); + if (byteString[0] == '?' && byteString[1] == '?') + { + // Both bytes are unknown. Set both the value and the mask to 0. + bytePatternArray[i / 2] = 0; + maskArray[i / 2] = 0; + } + else if (byteString[0] == '?') + { + // The first byte is unknown. Set the value to the second byte and the mask to 0xF. + bytePatternArray[i / 2] = Convert.ToByte(byteString[1].ToString(), 16); + maskArray[i / 2] = 0xF; + } + else if (byteString[1] == '?') + { + // The second byte is unknown. Set the value to the first byte multiplied by 16 and the mask to 0xF0. + bytePatternArray[i / 2] = (byte)(Convert.ToByte(byteString[0].ToString(), 16) * 16); + maskArray[i / 2] = 0xF0; + } + else + { + // Both bytes are known. Set the value to the byte and the mask to 0xFF. + bytePatternArray[i / 2] = Convert.ToByte(byteString, 16); + maskArray[i / 2] = 0xFF; + } + } + + return new Tuple(bytePatternArray, maskArray); + } + + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => _originalPatternString; + + /// + /// Implicitly converts a string to a . + /// + /// Pattern string to convert. + /// A new instance of created from the given pattern string. + /// Thrown when the pattern string is invalid. + public static implicit operator ByteSearchPattern(string patternString) => new(patternString); +} diff --git a/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs b/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs index 54a8e6a..1532a65 100644 --- a/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs +++ b/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs @@ -285,8 +285,8 @@ public void AlignedToTest(AlignedToTestCase testCase) /// [Test] public void AlignedToWithZeroAlignmentTest() - => Assert.That(() => new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1FFF)).AlignedTo(0), - Throws.ArgumentException); + => Assert.Throws(() => + new MemoryRange(new UIntPtr(0x1000), new UIntPtr(0x1FFF)).AlignedTo(0)); #endregion } \ No newline at end of file diff --git a/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs b/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs index 833db2d..74fb70f 100644 --- a/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs +++ b/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using MindControl.Results; using MindControl.Test.ProcessMemoryTests; using NUnit.Framework; @@ -22,7 +23,7 @@ public class MemoryAllocationTest : ProcessMemoryTest public void SetUp() { // Allocate a range of memory for the tests - _allocation = TestProcessMemory!.Allocate(0x1000, false); + _allocation = TestProcessMemory!.Allocate(0x1000, false).Value; } /// @@ -43,7 +44,7 @@ public void DisposeTest() Assert.That(TestProcessMemory!.Allocations, Is.Empty); // Check that the memory has been released (we should not be able to write to it) - Assert.Throws(() => TestProcessMemory.Write(address, 0, MemoryProtectionStrategy.Ignore)); + Assert.That(TestProcessMemory.Write(address, 0, MemoryProtectionStrategy.Ignore).IsSuccess, Is.False); } /// @@ -53,7 +54,10 @@ public void DisposeTest() [Test] public void ReserveRangeTest() { - var reservedRange = _allocation.ReserveRange(0x10); + var result = _allocation.ReserveRange(0x10); + Assert.That(result.IsSuccess, Is.True); + + var reservedRange = result.Value; // Check the reserved range Assert.That(reservedRange, Is.Not.Null); @@ -72,8 +76,8 @@ public void ReserveRangeTest() [Test] public void ReserveRangeWithFullRangeTest() { - // Just test that it doesn't throw. - _allocation.ReserveRange(_allocation.Range.GetSize()); + var result = _allocation.ReserveRange(_allocation.Range.GetSize()); + Assert.That(result.IsSuccess, Is.True); } /// @@ -86,22 +90,28 @@ public void ReserveRangeWithZeroSizeTest() /// /// Tests the method. - /// Attempt to reserve 1 byte more than the size of the full reservation range. This should throw. + /// Attempt to reserve 1 byte more than the size of the full reservation range. This should fail. /// [Test] public void ReserveRangeWithRangeLargerThanAllocationTest() - => Assert.Throws(() => _allocation.ReserveRange(0x1001)); - + { + var result = _allocation.ReserveRange(0x1001); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + } + /// /// Tests the method. /// Reserve 0x100 bytes, and then attempt to reserve 0x1000 bytes (full allocation range). - /// The second reservation should throw. + /// The second reservation should fail. /// [Test] public void ReserveRangeWithMultipleReservationsTooLargeToFitTest() { _allocation.ReserveRange(0x100); - Assert.Throws(() => _allocation.ReserveRange(0x1000)); + var result = _allocation.ReserveRange(0x1000); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); } /// @@ -112,8 +122,8 @@ public void ReserveRangeWithMultipleReservationsTooLargeToFitTest() [Test] public void ReserveRangeWithRealignmentTest() { - var a = _allocation.ReserveRange(5, byteAlignment: 4); - var b = _allocation.ReserveRange(5, byteAlignment: 4); + var a = _allocation.ReserveRange(5, byteAlignment: 4).Value; + var b = _allocation.ReserveRange(5, byteAlignment: 4).Value; Assert.That(a.Range, Is.EqualTo(new MemoryRange(_allocation.Range.Start, _allocation.Range.Start + 7))); Assert.That(b.Range, Is.EqualTo(new MemoryRange(_allocation.Range.Start + 8, _allocation.Range.Start + 15))); @@ -127,8 +137,8 @@ public void ReserveRangeWithRealignmentTest() [Test] public void ReserveRangeWithNoAlignmentTest() { - var a = _allocation.ReserveRange(5, byteAlignment: null); - var b = _allocation.ReserveRange(5, byteAlignment: null); + var a = _allocation.ReserveRange(5, byteAlignment: null).Value; + var b = _allocation.ReserveRange(5, byteAlignment: null).Value; Assert.That(a.Range, Is.EqualTo(new MemoryRange(_allocation.Range.Start, _allocation.Range.Start + 4))); Assert.That(b.Range, Is.EqualTo(new MemoryRange(_allocation.Range.Start + 5, _allocation.Range.Start + 9))); @@ -144,7 +154,7 @@ public void GetRemainingSpaceTest() // Make 3 0x10 allocations and check that the remaining space is total space - 0x30 _allocation.ReserveRange(0x10); - var b = _allocation.ReserveRange(0x10); + var b = _allocation.ReserveRange(0x10).Value; _allocation.ReserveRange(0x10); Assert.That(_allocation.GetRemainingSpace(), Is.EqualTo(_allocation.Range.GetSize() - 0x30)); @@ -163,7 +173,7 @@ public void GetTotalReservedSpaceTest() // Make 3 0x10 allocations and check that the reserved space is 0x30 _allocation.ReserveRange(0x10); - var b = _allocation.ReserveRange(0x10); + var b = _allocation.ReserveRange(0x10).Value; _allocation.ReserveRange(0x10); Assert.That(_allocation.GetTotalReservedSpace(), Is.EqualTo(0x30)); @@ -182,7 +192,7 @@ public void GetLargestReservableSpaceTest() // Make 3 0x500 allocations. The largest unreserved space should now be the last 0x100 bytes of the range. _allocation.ReserveRange(0x500); - var b = _allocation.ReserveRange(0x500); + var b = _allocation.ReserveRange(0x500).Value; _allocation.ReserveRange(0x500); var expectedRange = new MemoryRange(_allocation.Range.End - 0xFF, _allocation.Range.End); Assert.That(_allocation.GetLargestReservableSpace(), Is.EqualTo(expectedRange)); @@ -213,7 +223,7 @@ public void GetNextRangeFittingSizeTest() // Make 3 0x10 allocations. The next available 0x10 space should be right after these three. _allocation.ReserveRange(0x10); - var b = _allocation.ReserveRange(0x10); + var b = _allocation.ReserveRange(0x10).Value; _allocation.ReserveRange(0x10); Assert.That(_allocation.GetNextRangeFittingSize(0x10), Is.EqualTo(new MemoryRange(_allocation.Range.Start + 0x30, _allocation.Range.Start + 0x3F))); @@ -278,7 +288,7 @@ public void GetNextRangeFittingSizeWithNoAlignmentTest() [Test] public void FreeRangeWithFullReservationRangeTest() { - var reservation = _allocation.ReserveRange(0x10); + var reservation = _allocation.ReserveRange(0x10).Value; _allocation.FreeRange(reservation.Range); Assert.That(reservation.IsDisposed, Is.True); @@ -294,7 +304,7 @@ public void FreeRangeWithFullReservationRangeTest() [Test] public void FreeRangeWithRangeOverlappingStartOfExistingReservationTest() { - var reservation = _allocation.ReserveRange(0x10); + var reservation = _allocation.ReserveRange(0x10).Value; _allocation.FreeRange(new MemoryRange(reservation.Range.Start - 4, reservation.Range.Start + 4)); Assert.That(reservation.IsDisposed, Is.True); @@ -312,7 +322,7 @@ public void FreeRangeWithRangeOverlappingStartOfExistingReservationTest() [Test] public void FreeRangeWithRangeOverlappingEndOfExistingReservationTest() { - var reservation = _allocation.ReserveRange(0x10); + var reservation = _allocation.ReserveRange(0x10).Value; _allocation.FreeRange(new MemoryRange(reservation.Range.Start + 4, reservation.Range.Start + 0xF)); Assert.That(reservation.IsDisposed, Is.True); @@ -330,7 +340,7 @@ public void FreeRangeWithRangeOverlappingEndOfExistingReservationTest() [Test] public void FreeRangeWithRangeInsideExistingReservationTest() { - var reservation = _allocation.ReserveRange(0x10); + var reservation = _allocation.ReserveRange(0x10).Value; _allocation.FreeRange(new MemoryRange(reservation.Range.Start + 4, reservation.Range.Start + 6)); Assert.That(reservation.IsDisposed, Is.True); @@ -351,9 +361,9 @@ public void FreeRangeWithRangeInsideExistingReservationTest() [Test] public void FreeRangeWithRangeOverlappingMultipleExistingReservationsTest() { - var a = _allocation.ReserveRange(0x10); - var b = _allocation.ReserveRange(0x10); - var c = _allocation.ReserveRange(0x10); + var a = _allocation.ReserveRange(0x10).Value; + var b = _allocation.ReserveRange(0x10).Value; + var c = _allocation.ReserveRange(0x10).Value; _allocation.FreeRange(new MemoryRange(a.Range.Start + 8, b.Range.End)); Assert.That(a.IsDisposed, Is.True); @@ -373,9 +383,9 @@ public void FreeRangeWithRangeOverlappingMultipleExistingReservationsTest() [Test] public void ClearReservationsTest() { - var a = _allocation.ReserveRange(0x10); - var b = _allocation.ReserveRange(0x10); - var c = _allocation.ReserveRange(0x10); + var a = _allocation.ReserveRange(0x10).Value; + var b = _allocation.ReserveRange(0x10).Value; + var c = _allocation.ReserveRange(0x10).Value; _allocation.ClearReservations(); Assert.That(a.IsDisposed, Is.True); diff --git a/test/MindControl.Test/AllocationTests/MemoryReservationTest.cs b/test/MindControl.Test/AllocationTests/MemoryReservationTest.cs index 391afa5..48842e9 100644 --- a/test/MindControl.Test/AllocationTests/MemoryReservationTest.cs +++ b/test/MindControl.Test/AllocationTests/MemoryReservationTest.cs @@ -22,10 +22,10 @@ public class MemoryReservationTest : ProcessMemoryTest public void SetUp() { // Allocate a range of memory for the tests - _allocation = TestProcessMemory!.Allocate(0x1000, false); + _allocation = TestProcessMemory!.Allocate(0x1000, false).Value; // Reserve a portion of the memory - _reservation = _allocation.ReserveRange(0x10); + _reservation = _allocation.ReserveRange(0x10).Value; } /// diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index 6e55fb8..07586ab 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -16,7 +16,10 @@ public class ProcessMemoryAllocationTest : ProcessMemoryTest [Test] public void AllocateTest() { - var allocation = TestProcessMemory!.Allocate(0x1000, false); + var allocationResult = TestProcessMemory!.Allocate(0x1000, false); + + Assert.That(allocationResult.IsSuccess, Is.True); + var allocation = allocationResult.Value; // To check that the memory is writable, we will write a byte array to the allocated range. // We will use the WriteBytes method rather than Store, because Store is built on top of Allocate, and we only @@ -37,7 +40,10 @@ public void AllocateTest() [Test] public void AllocateExecutableTest() { - var allocation = TestProcessMemory!.Allocate(0x1000, true); + var allocationResult = TestProcessMemory!.Allocate(0x1000, true); + + Assert.That(allocationResult.IsSuccess, Is.True); + var allocation = allocationResult.Value; Assert.That(allocation, Is.Not.Null); Assert.That(allocation.IsDisposed, Is.False); Assert.That(allocation.Range.GetSize(), Is.AtLeast(0x1000)); @@ -61,10 +67,12 @@ public void AllocateZeroTest() public void StoreWithRangeTest() { var value = new byte[] { 1, 2, 3, 4 }; - var allocation = TestProcessMemory!.Allocate(0x1000, false); + var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; - var reservation = TestProcessMemory.Store(value, allocation); - var read = TestProcessMemory.ReadBytes(reservation.Range.Start, value.Length); + var reservationResult = TestProcessMemory.Store(value, allocation); + Assert.That(reservationResult.IsSuccess, Is.True); + var reservation = reservationResult.Value; + byte[] read = TestProcessMemory.ReadBytes(reservation.Range.Start, value.Length).Value; Assert.That(reservation.IsDisposed, Is.False); // The resulting range should be a range reserved from our original range. @@ -83,8 +91,10 @@ public void StoreWithoutPreAllocationTest() { var value = new byte[] { 1, 2, 3, 4 }; - var reservation = TestProcessMemory!.Store(value); - var read = TestProcessMemory.ReadBytes(reservation.Range.Start, value.Length); + var reservationResult = TestProcessMemory!.Store(value); + Assert.That(reservationResult.IsSuccess, Is.True); + var reservation = reservationResult.Value; + var read = TestProcessMemory.ReadBytes(reservation.Range.Start, value.Length).Value; // The store method should have allocated a new range. Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); @@ -103,8 +113,13 @@ public void StoreWithoutPreAllocationWithMultipleSmallValuesTest() { var value = new byte[] { 1, 2, 3, 4 }; - var reservations = Enumerable.Range(0, 4).Select(_ => TestProcessMemory!.Store(value)).ToList(); - var readBackValues = reservations.Select(r => TestProcessMemory!.ReadBytes(r.Range.Start, value.Length)); + var reservationResults = Enumerable.Range(0, 4).Select(_ => TestProcessMemory!.Store(value)).ToList(); + foreach (var result in reservationResults) + Assert.That(result.IsSuccess, Is.True); + + var reservations = reservationResults.Select(r => r.Value).ToList(); + var readBackValues = reservations.Select(r => TestProcessMemory!.ReadBytes(r.Range.Start, value.Length) + .GetValueOrDefault()); // The store method should have allocated only one range that's big enough to accomodate all the values. Assert.That(TestProcessMemory!.Allocations, Has.Count.EqualTo(1)); @@ -126,18 +141,20 @@ public void StoreWithoutPreAllocationWithMultipleSmallValuesTest() [Test] public void StoreWithMultipleOverflowingValuesTest() { - var allocation = TestProcessMemory!.Allocate(0x1000, false); + var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; var value = new byte[allocation.Range.GetSize()]; - TestProcessMemory!.Store(value); + var firstStoreResult = TestProcessMemory!.Store(value); // So far, we should have only one allocated range. + Assert.That(firstStoreResult.IsSuccess, Is.True); Assert.That(TestProcessMemory!.Allocations, Has.Count.EqualTo(1)); // Now we store the same value again, which should overflow the range. - TestProcessMemory!.Store(value); + var secondStoreResult = TestProcessMemory!.Store(value); // We should have two allocated ranges now, because there is no room left in the first range. + Assert.That(secondStoreResult.IsSuccess, Is.True); Assert.That(TestProcessMemory!.Allocations, Has.Count.EqualTo(2)); } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs index 061bbcc..fbb65ad 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +using System.Numerics; +using MindControl.Results; +using NUnit.Framework; namespace MindControl.Test.ProcessMemoryTests; @@ -8,7 +10,94 @@ namespace MindControl.Test.ProcessMemoryTests; /// Most pointer path evaluation features are implicitly tested through memory reading and writing methods. /// This test class focuses on special cases. [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryEvaluateTest +public class ProcessMemoryEvaluateTest : ProcessMemoryTest { + /// + /// Tests the nominal case, with a path that evaluates to a valid address. + /// + [Test] + public void EvaluateOnKnownPointerTest() + { + // This path is known to point to 0xFFFFFFFFFFFFFFFF (i.e. the max 8-byte value). + var result = TestProcessMemory!.EvaluateMemoryAddress($"{OuterClassPointer:X}+10,10,0"); + + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo((UIntPtr)ulong.MaxValue)); + } + /// + /// Tests an error case where the pointer path points to a value located after the last possible byte in memory + /// (the maximum value of a UIntPtr + 1). + /// The operation is expected to fail with a . + /// + [Test] + public void EvaluateOverMaxPointerValueTest() + { + var expectedPreviousAddress = TestProcessMemory!.EvaluateMemoryAddress($"{OuterClassPointer:X}+10,10") + .GetValueOrDefault(); + + var result = TestProcessMemory!.EvaluateMemoryAddress($"{OuterClassPointer:X}+10,10,1"); + + Assert.That(result.IsSuccess, Is.False); + var error = result.Error; + Assert.That(error, Is.TypeOf()); + var pathError = (PathEvaluationFailureOnPointerOutOfRange)error; + Assert.That(pathError.Address, Is.EqualTo(new BigInteger(ulong.MaxValue) + 1)); + Assert.That(pathError.PreviousAddress, Is.EqualTo(expectedPreviousAddress)); + } + + /// + /// Tests an error case where the pointer path points to a value that is not readable. + /// The operation is expected to fail with a . + /// + [Test] + public void EvaluateWithUnreadableAddressTest() + { + // This path will try to follow a pointer to 0xFFFFFFFFFFFFFFFF, which is not readable + var result = TestProcessMemory!.EvaluateMemoryAddress($"{OuterClassPointer:X}+10,10,0,0"); + + Assert.That(result.IsSuccess, Is.False); + var error = result.Error; + Assert.That(error, Is.TypeOf()); + var pathError = (PathEvaluationFailureOnPointerReadFailure)error; + Assert.That(pathError.Address, Is.EqualTo((UIntPtr)ulong.MaxValue)); + Assert.That(pathError.Failure, Is.TypeOf()); + var readFailure = (ReadFailureOnSystemRead)pathError.Failure; + Assert.That(readFailure.SystemReadFailure, Is.TypeOf()); + var osFailure = (OperatingSystemCallFailure)readFailure.SystemReadFailure; + Assert.That(osFailure.ErrorCode, Is.GreaterThan(0)); + Assert.That(osFailure.ErrorMessage, Is.Not.Empty); + } + + /// + /// Tests an error case where the pointer path given to a read operation points to zero. + /// The operation is expected to fail with a . + /// + [Test] + public void EvaluateOnZeroPointerTest() + { + var result = TestProcessMemory!.EvaluateMemoryAddress("0"); + + Assert.That(result.IsSuccess, Is.False); + var error = result.Error; + Assert.That(error, Is.TypeOf()); + var pathError = (PathEvaluationFailureOnPointerOutOfRange)error; + Assert.That(pathError.Address, Is.EqualTo(new BigInteger(0))); + Assert.That(pathError.PreviousAddress, Is.Null); + } + + /// + /// Tests an error case where the pointer path has a module that is not part of the target process. + /// The operation is expected to fail with a . + /// + [Test] + public void EvaluateWithUnknownModuleTest() + { + var result = TestProcessMemory!.EvaluateMemoryAddress("ThisModuleDoesNotExist.dll+10,10"); + + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + var error = (PathEvaluationFailureOnBaseModuleNotFound)result.Error; + Assert.That(error.ModuleName, Is.EqualTo("ThisModuleDoesNotExist.dll")); + } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs index 4595153..dc83884 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using MindControl.Results; +using NUnit.Framework; namespace MindControl.Test.ProcessMemoryTests; @@ -30,7 +31,8 @@ public void SetUp() [Test] public void InjectLibraryTest() { - TestProcessMemory!.InjectLibrary(InjectedLibraryPath); + var result = TestProcessMemory!.InjectLibrary(InjectedLibraryPath); + Assert.That(result.IsSuccess, Is.True); var output = ProceedToNextStep(); Assert.That(output, Is.EqualTo("Injected library attached")); } @@ -45,8 +47,26 @@ public void InjectLibraryWithNonAsciiPathTest() { const string targetPath = "憂 鬱.dll"; File.Copy(InjectedLibraryPath, targetPath, true); - TestProcessMemory!.InjectLibrary(targetPath); + var result = TestProcessMemory!.InjectLibrary(targetPath); + Assert.That(result.IsSuccess, Is.True); var output = ProceedToNextStep(); Assert.That(output, Is.EqualTo("Injected library attached")); } + + /// + /// Tests the method. + /// Specify a path to a non-existent library file. + /// The method should fail with a . + /// + [Test] + public void InjectLibraryWithLibraryFileNotFoundTest() + { + const string path = "./NonExistentLibrary.dll"; + var result = TestProcessMemory!.InjectLibrary(path); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + var error = (InjectionFailureOnLibraryFileNotFound)result.Error; + Assert.That(error.LibraryPath, Has.Length.GreaterThan(path.Length)); // We expect a full path + Assert.That(error.LibraryPath, Does.EndWith("NonExistentLibrary.dll")); + } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index 350dacf..917d4ad 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -1,4 +1,6 @@ -using System.Text; +using System.Numerics; +using System.Text; +using MindControl.Results; using NUnit.Framework; namespace MindControl.Test.ProcessMemoryTests; @@ -33,10 +35,10 @@ public void ReadTwoStepGenericTest(Type targetType, int pointerOffset, object? e object? expectedResultAfterBreak) { UIntPtr targetIntAddress = OuterClassPointer + pointerOffset; - object? resultBefore = TestProcessMemory!.Read(targetType, targetIntAddress); + object resultBefore = TestProcessMemory!.Read(targetType, targetIntAddress).Value; Assert.That(resultBefore, Is.EqualTo(expectedResultBeforeBreak)); ProceedToNextStep(); - object? resultAfter = TestProcessMemory.Read(targetType, targetIntAddress); + object resultAfter = TestProcessMemory.Read(targetType, targetIntAddress).Value; Assert.That(resultAfter, Is.EqualTo(expectedResultAfterBreak)); } @@ -46,7 +48,8 @@ public void ReadTwoStepGenericTest(Type targetType, int pointerOffset, object? e /// It should be equal to its known value. /// [Test] - public void ReadBoolTest() => Assert.That(TestProcessMemory!.ReadBool(OuterClassPointer + 0x48), Is.EqualTo(true)); + public void ReadBoolTest() + => Assert.That(TestProcessMemory!.ReadBool(OuterClassPointer + 0x48).GetValueOrDefault(), Is.EqualTo(true)); /// /// Tests . @@ -54,7 +57,8 @@ public void ReadTwoStepGenericTest(Type targetType, int pointerOffset, object? e /// It should be equal to its known value. /// [Test] - public void ReadByteTest() => Assert.That(TestProcessMemory!.ReadByte(OuterClassPointer + 0x49), Is.EqualTo(0xAC)); + public void ReadByteTest() + => Assert.That(TestProcessMemory!.ReadByte(OuterClassPointer + 0x49).GetValueOrDefault(), Is.EqualTo(0xAC)); /// /// Tests . @@ -63,7 +67,7 @@ public void ReadTwoStepGenericTest(Type targetType, int pointerOffset, object? e /// [Test] public void ReadShortTest() => Assert.That( - TestProcessMemory!.ReadShort(OuterClassPointer + 0x44), Is.EqualTo(-7777)); + TestProcessMemory!.ReadShort(OuterClassPointer + 0x44).GetValueOrDefault(), Is.EqualTo(-7777)); /// /// Tests . @@ -72,7 +76,7 @@ public void ReadShortTest() => Assert.That( /// [Test] public void ReadUShortTest() => Assert.That( - TestProcessMemory!.ReadUShort(OuterClassPointer + 0x46), Is.EqualTo(8888)); + TestProcessMemory!.ReadUShort(OuterClassPointer + 0x46).GetValueOrDefault(), Is.EqualTo(8888)); /// /// Tests . @@ -80,7 +84,8 @@ public void ReadUShortTest() => Assert.That( /// It should be equal to its known value. /// [Test] - public void ReadIntTest() => Assert.That(TestProcessMemory!.ReadInt(OuterClassPointer + 0x38), Is.EqualTo(-7651)); + public void ReadIntTest() + => Assert.That(TestProcessMemory!.ReadInt(OuterClassPointer + 0x38).GetValueOrDefault(), Is.EqualTo(-7651)); /// /// Tests . @@ -89,7 +94,7 @@ public void ReadUShortTest() => Assert.That( /// [Test] public void ReadUIntTest() => Assert.That( - TestProcessMemory!.ReadUInt(OuterClassPointer + 0x3C), Is.EqualTo(6781631)); + TestProcessMemory!.ReadUInt(OuterClassPointer + 0x3C).GetValueOrDefault(), Is.EqualTo(6781631)); /// /// Tests . @@ -98,7 +103,7 @@ public void ReadUIntTest() => Assert.That( /// [Test] public void ReadLongTest() => Assert.That( - TestProcessMemory!.ReadLong(OuterClassPointer + 0x20), Is.EqualTo(-65746876815103L)); + TestProcessMemory!.ReadLong(OuterClassPointer + 0x20).GetValueOrDefault(), Is.EqualTo(-65746876815103L)); /// /// Tests . @@ -107,7 +112,7 @@ public void ReadLongTest() => Assert.That( /// [Test] public void ReadULongTest() => Assert.That( - TestProcessMemory!.ReadULong(OuterClassPointer + 0x28), Is.EqualTo(76354111324644L)); + TestProcessMemory!.ReadULong(OuterClassPointer + 0x28).GetValueOrDefault(), Is.EqualTo(76354111324644L)); /// /// Tests . @@ -116,7 +121,7 @@ public void ReadULongTest() => Assert.That( /// [Test] public void ReadFloatTest() => Assert.That( - TestProcessMemory!.ReadFloat(OuterClassPointer + 0x40), Is.EqualTo(3456765.323f)); + TestProcessMemory!.ReadFloat(OuterClassPointer + 0x40).GetValueOrDefault(), Is.EqualTo(3456765.323f)); /// /// Tests . @@ -125,7 +130,7 @@ public void ReadFloatTest() => Assert.That( /// [Test] public void ReadDoubleTest() => Assert.That( - TestProcessMemory!.ReadDouble(OuterClassPointer + 0x30), Is.EqualTo(79879131651.33345)); + TestProcessMemory!.ReadDouble(OuterClassPointer + 0x30).GetValueOrDefault(), Is.EqualTo(79879131651.33345)); /// /// Tests . @@ -134,7 +139,7 @@ public void ReadDoubleTest() => Assert.That( /// [Test] public void ReadBytesTest() => Assert.That( - TestProcessMemory!.ReadBytes($"{OuterClassPointer:X}+18,10", 4), + TestProcessMemory!.ReadBytes($"{OuterClassPointer:X}+18,10", 4).GetValueOrDefault(), Is.EqualTo(new byte[] { 0x11, 0x22, 0x33, 0x44 })); /// @@ -145,7 +150,7 @@ public void ReadBytesTest() => Assert.That( /// [Test] public void ReadNestedLongTest() => Assert.That( - TestProcessMemory!.ReadLong($"{OuterClassPointer:X}+10,8"), Is.EqualTo(999999999999L)); + TestProcessMemory!.ReadLong($"{OuterClassPointer:X}+10,8").GetValueOrDefault(), Is.EqualTo(999999999999L)); /// /// Tests . @@ -157,32 +162,47 @@ public void ReadNestedLongTest() => Assert.That( [Test] public void ReadUIntPtrMaxValueTest() { - var ptr = TestProcessMemory!.ReadIntPtr($"{OuterClassPointer:X}+10,10"); - Assert.That(ptr.GetValueOrDefault().ToUInt64(), Is.EqualTo(ulong.MaxValue)); + var ptr = TestProcessMemory!.ReadIntPtr($"{OuterClassPointer:X}+10,10").GetValueOrDefault(); + Assert.That(ptr.ToUInt64(), Is.EqualTo(ulong.MaxValue)); } /// /// Tests an edge case where the pointer path given to a read operation points to the last possible byte in memory /// (the maximum value of a UIntPtr). - /// The read operation is expected to fail gracefully and return null (this memory region is not valid). + /// The read operation is expected to fail (this memory region is not valid). /// [Test] public void ReadAtMaxPointerValueTest() { var result = TestProcessMemory!.ReadByte($"{OuterClassPointer:X}+10,10,0"); - Assert.That(result, Is.Null); + Assert.That(result.IsSuccess, Is.False); + var error = result.Error; + Assert.That(error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); + var systemError = ((ReadFailureOnSystemRead)error).SystemReadFailure; + Assert.That(systemError, Is.TypeOf(typeof(OperatingSystemCallFailure))); + var osError = ((OperatingSystemCallFailure)systemError); + Assert.That(osError.ErrorCode, Is.GreaterThan(0)); + Assert.That(osError.ErrorMessage, Is.Not.Empty); } /// /// Tests an edge case where the pointer path given to a read operation points to a value located after the last /// possible byte in memory (the maximum value of a UIntPtr + 1). - /// The read operation is expected to fail gracefully and return null (the target pointer is not addressable). + /// The read operation is expected to fail with a . /// [Test] public void ReadOverMaxPointerValueTest() { var result = TestProcessMemory!.ReadByte($"{OuterClassPointer:X}+10,10,1"); - Assert.That(result, Is.Null); + Assert.That(result.IsSuccess, Is.False); + var error = result.Error; + Assert.That(error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); + var pathError = ((ReadFailureOnPointerPathEvaluation)error).PathEvaluationFailure; + Assert.That(pathError, Is.TypeOf(typeof(PathEvaluationFailureOnPointerOutOfRange))); + var outOfRangeError = (PathEvaluationFailureOnPointerOutOfRange)pathError; + Assert.That(outOfRangeError.Address, Is.EqualTo(new BigInteger(ulong.MaxValue) + 1)); + Assert.That(outOfRangeError.PreviousAddress, Is.Not.Null); + Assert.That(outOfRangeError.PreviousAddress, Is.Not.EqualTo(UIntPtr.Zero)); } /// @@ -196,9 +216,10 @@ public void ReadOverMaxPointerValueTest() public void ReadStringWithNoParametersTest() { var pointerPath = new PointerPath($"{OuterClassPointer:X}+8,8"); - Assert.That(TestProcessMemory!.ReadString(pointerPath), Is.EqualTo("ThisIsÄString")); + Assert.That(TestProcessMemory!.ReadString(pointerPath).GetValueOrDefault(), Is.EqualTo("ThisIsÄString")); ProceedToNextStep(); - Assert.That(TestProcessMemory!.ReadString(pointerPath), Is.EqualTo("ThisIsALongerStrîngWith文字化けチェック")); + Assert.That(TestProcessMemory!.ReadString(pointerPath).GetValueOrDefault(), + Is.EqualTo("ThisIsALongerStrîngWith文字化けチェック")); } /// @@ -211,7 +232,8 @@ public void ReadStringWithNoParametersTest() /// [Test] public void ReadStringWithLimitedLengthTest() - => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", 10), Is.EqualTo("ThisI")); + => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", 10).GetValueOrDefault(), + Is.EqualTo("ThisI")); /// /// Tests . @@ -225,7 +247,8 @@ public void ReadStringWithLimitedLengthTest() [Test] public void ReadStringWithoutLengthPrefixTest() => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,C", - stringSettings: new StringSettings(Encoding.Unicode, true, null)), Is.EqualTo("ThisIsÄString")); + stringSettings: new StringSettings(Encoding.Unicode, true, null)).GetValueOrDefault(), + Is.EqualTo("ThisIsÄString")); /// /// Tests . @@ -239,7 +262,8 @@ public void ReadStringWithoutLengthPrefixTest() [Test] public void ReadStringWithLengthPrefixWithoutNullTerminatorTest() => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", - stringSettings: new StringSettings(Encoding.Unicode, false, new StringLengthPrefixSettings(4, 2))), + stringSettings: new StringSettings(Encoding.Unicode, false, new StringLengthPrefixSettings(4, 2))) + .GetValueOrDefault(), Is.EqualTo("ThisIsÄString")); /// @@ -255,8 +279,8 @@ public void ReadStringWithLengthPrefixWithoutNullTerminatorTest() [Test] public void ReadStringWithoutLengthPrefixOrNullTerminatorTest() { - var result = TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,C", 64, - new StringSettings(Encoding.Unicode, false, null)); + string? result = TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,C", 64, + new StringSettings(Encoding.Unicode, false, null)).GetValueOrDefault(); // We should have a string that starts with the full known string, and has at least one more character. // We cannot test an exact string or length because the memory region after the string is not guaranteed to @@ -276,7 +300,8 @@ public void ReadStringWithoutLengthPrefixOrNullTerminatorTest() [Test] public void ReadStringWithUnspecifiedLengthPrefixUnitTest() => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", - stringSettings: new StringSettings(Encoding.Unicode, false, new StringLengthPrefixSettings(4))), + stringSettings: new StringSettings(Encoding.Unicode, false, new StringLengthPrefixSettings(4))) + .GetValueOrDefault(), Is.EqualTo("ThisIsÄString")); /// @@ -290,7 +315,8 @@ public void ReadStringWithUnspecifiedLengthPrefixUnitTest() [Test] public void ReadStringWithZeroLengthPrefixUnitTest() => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,A", - stringSettings: new StringSettings(Encoding.Unicode, true, new StringLengthPrefixSettings(2, 2))), + stringSettings: new StringSettings(Encoding.Unicode, true, new StringLengthPrefixSettings(2, 2))) + .GetValueOrDefault(), Is.EqualTo(string.Empty)); /// @@ -308,6 +334,7 @@ public void ReadStringWithZeroLengthPrefixUnitTest() [Test] public void ReadStringWithWrongEncodingTest() => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", - stringSettings: new StringSettings(Encoding.UTF8, true, new StringLengthPrefixSettings(4, 2))), + stringSettings: new StringSettings(Encoding.UTF8, true, new StringLengthPrefixSettings(4, 2))) + .GetValueOrDefault(), Is.EqualTo("T")); } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs index bc5ca4d..c58c6e1 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs @@ -13,7 +13,7 @@ public class ProcessMemoryWriteTest : ProcessMemoryTest /// /// Stores the line-by-line expected output of the target app. /// - private static string[] ExpectedFinalValues = + private static readonly string[] ExpectedFinalValues = { "False", "220", @@ -92,8 +92,10 @@ public void WriteStructTest() ProceedToNextStep(); var pointerPath = new PointerPath($"{OuterClassPointer:X}+50"); TestProcessMemory!.Write(pointerPath, structInstance); - var valueReadBack = TestProcessMemory.Read(pointerPath); - Assert.That(valueReadBack, Is.EqualTo(structInstance)); + var readBackResult = TestProcessMemory.Read(pointerPath); + + Assert.That(readBackResult.IsSuccess, Is.True); + Assert.That(readBackResult.Value, Is.EqualTo(structInstance)); } } \ No newline at end of file diff --git a/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs b/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs new file mode 100644 index 0000000..ade1f59 --- /dev/null +++ b/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs @@ -0,0 +1,102 @@ +using NUnit.Framework; + +namespace MindControl.Test.SearchTests; + +/// +/// Tests the class. +/// +public class ByteSearchPatternTest +{ + public readonly record struct PatternExpectedResult(byte[] ByteArray, byte[] Mask); + + public readonly record struct PatternTestCase(string Pattern, PatternExpectedResult? Expected); + + private static readonly PatternTestCase[] TestCases = + { + // Nominal case: only full bytes + new("1F 49 1A 03", new PatternExpectedResult( + new byte[] { 0x1F, 0x49, 0x1A, 0x03 }, + new byte[] { 0xFF, 0xFF, 0xFF, 0xFF })), + + // Full bytes and full wildcards + new("1F ?? 1A 03", new PatternExpectedResult( + new byte[] { 0x1F, 0x00, 0x1A, 0x03 }, + new byte[] { 0xFF, 0x00, 0xFF, 0xFF })), + + // Partial wildcard (left) + new("1F ?9 1A 03", new PatternExpectedResult( + new byte[] { 0x1F, 0x09, 0x1A, 0x03 }, + new byte[] { 0xFF, 0x0F, 0xFF, 0xFF })), + + // Partial wildcard (right) + new("1F 4? 1A 03", new PatternExpectedResult( + new byte[] { 0x1F, 0x40, 0x1A, 0x03 }, + new byte[] { 0xFF, 0xF0, 0xFF, 0xFF })), + + // Mixed (all cases and odd spaces) + new("1F4? ?A??", new PatternExpectedResult( + new byte[] { 0x1F, 0x40, 0x0A, 0x00 }, + new byte[] { 0xFF, 0xF0, 0x0F, 0x00 })), + + // Error case: odd number of characters + new("1F 49 1A 0", null), + + // Error case: invalid character + new("1F 49 1A 0G", null), + + // Error case: only wildcards + new("?? ?? ?? ??", null), + + // Error case: empty string + new(" ", null) + }; + + /// + /// Tests the method. + /// + [TestCaseSource(nameof(TestCases))] + public void TryParseTest(PatternTestCase testCase) + { + var result = ByteSearchPattern.TryParse(testCase.Pattern); + if (testCase.Expected == null) + { + Assert.That(result.IsFailure, Is.True); + Assert.That(result.Error.Message, Is.Not.Empty); + } + else + { + Assert.That(result.IsSuccess, Is.True); + var pattern = result.Value; + Assert.That(pattern.ToString(), Is.EqualTo(testCase.Pattern)); + Assert.That(pattern.ByteArray, Is.EqualTo(testCase.Expected.Value.ByteArray)); + Assert.That(pattern.Mask, Is.EqualTo(testCase.Expected.Value.Mask)); + } + } + + /// + /// Tests the constructor. + /// + [TestCaseSource(nameof(TestCases))] + public void ConstructorTest(PatternTestCase testCase) + { + ByteSearchPattern pattern; + try + { + pattern = new ByteSearchPattern(testCase.Pattern); + } + catch (Exception e) + { + if (testCase.Expected != null) + Assert.Fail($"Unexpected exception: {e}"); + + return; + } + + if (testCase.Expected == null) + Assert.Fail("Expected exception not thrown."); + + Assert.That(pattern.ToString(), Is.EqualTo(testCase.Pattern)); + Assert.That(pattern.ByteArray, Is.EqualTo(testCase.Expected!.Value.ByteArray)); + Assert.That(pattern.Mask, Is.EqualTo(testCase.Expected.Value.Mask)); + } +} \ No newline at end of file From 44a87ba39b1ff05126d88f27f56421348f64fb27 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Tue, 21 May 2024 21:16:47 +0200 Subject: [PATCH 08/66] Removed type-specific Read methods --- .../ProcessMemory/ProcessMemory.Addressing.cs | 2 +- .../ProcessMemory/ProcessMemory.Read.cs | 380 ++++-------------- src/MindControl/Results/ReadFailure.cs | 22 +- .../ProcessMemoryReadTest.cs | 266 ++++++++---- 4 files changed, 288 insertions(+), 382 deletions(-) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs index 7c5d07c..a8a498a 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs @@ -52,7 +52,7 @@ public Result EvaluateMemoryAddress(PointerPath for (int i = startIndex; i < pointerPath.PointerOffsets.Length; i++) { // Read the value pointed by the current address as a pointer address - var nextAddressResult = ReadIntPtr(currentAddress); + var nextAddressResult = Read(currentAddress); if (nextAddressResult.IsFailure) return new PathEvaluationFailureOnPointerReadFailure(currentAddress, nextAddressResult.Error); diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs index ee0b638..6d5c7b5 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs @@ -9,85 +9,115 @@ namespace MindControl; public partial class ProcessMemory { #region Public methods - + /// /// Reads a specific type of data from the address referred by the given pointer path, in the process memory. + /// This method only supports value types (primitive types and structures). /// - /// Optimized, reusable path to the target address. - /// Type of data to read. Some types are not supported and will cause the method to throw - /// an . Do not use Nullable types. + /// Pointer path to the target address. Can be implicitly converted from a string. + /// Example: "MyGame.exe+1F1688,1F,4". Reuse instances to optimize execution time. + /// Type of data to read. Only value types are supported (primitive types and structures). + /// /// The value read from the process memory, or a read failure. - public Result Read(PointerPath pointerPath) + public Result Read(PointerPath pointerPath) where T : struct { - var objectReadResult = Read(typeof(T), pointerPath); - return objectReadResult.IsSuccess ? (T)objectReadResult.Value : objectReadResult.Error; + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? Read(addressResult.Value) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); } - + /// - /// Reads a specific type of data from the given address, in the process memory. + /// Reads a specific type of data from the given address, in the process memory. This method only supports value + /// types (primitive types and structures). /// /// Target address in the process memory. - /// Type of data to read. Some types are not supported and will cause the method to throw - /// an . Do not use reference (nullable) types. + /// Type of data to read. Only value types are supported (primitive types and structures). + /// /// The value read from the process memory, or a read failure. - public Result Read(UIntPtr address) + public Result Read(UIntPtr address) where T : struct { - var objectReadResult = Read(typeof(T), address); - return objectReadResult.IsSuccess ? (T)objectReadResult.Value : objectReadResult.Error; + // Check the address + if (address == UIntPtr.Zero) + return new ReadFailureOnZeroPointer(); + if (!IsBitnessCompatible(address)) + return new ReadFailureOnIncompatibleBitness(address); + + // Get the size of the target type + int size = Marshal.SizeOf(); + + // Read the bytes from the process memory + var readResult = _osService.ReadProcessMemory(_processHandle, address, (ulong)size); + if (readResult.IsFailure) + return new ReadFailureOnSystemRead(readResult.Error); + byte[] bytes = readResult.Value; + + // Convert the bytes into the target type + try + { + return MemoryMarshal.Read(bytes); + } + catch (Exception) + { + return new ReadFailureOnConversionFailure(); + } } /// /// Reads a specific type of data from the address referred by the given pointer path, in the process memory. + /// This method only supports value types (primitive types and structures). /// - /// Optimized, reusable path to the target address. - /// Type of data to read. Some types are not supported and will cause the method to throw - /// an . Do not use reference (nullable) types. + /// Type of data to read. Only value types are supported (primitive types and structures). + /// + /// Pointer path to the target address. Can be implicitly converted from a string. + /// Example: "MyGame.exe+1F1688,1F,4". Reuse instances to optimize execution time. /// The value read from the process memory, or a read failure. - public Result Read(Type dataType, PointerPath pointerPath) + public Result Read(Type type, PointerPath pointerPath) { var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? Read(dataType, addressResult.Value) + return addressResult.IsSuccess ? Read(type, addressResult.Value) : new ReadFailureOnPointerPathEvaluation(addressResult.Error); } - + /// - /// Reads a specific type of data from the given address, in the process memory. + /// Reads a specific type of data from the given address, in the process memory. This method only supports value + /// types (primitive types and structures). /// + /// Type of data to read. Only value types are supported (primitive types and structures). + /// /// Target address in the process memory. - /// Type of data to read. Some types are not supported and will cause the method to throw - /// an . Do not use Nullable types. /// The value read from the process memory, or a read failure. - public Result Read(Type dataType, UIntPtr address) + public Result Read(Type type, UIntPtr address) { - if (dataType == typeof(bool)) return Result.CastValueFrom(ReadBool(address)); - if (dataType == typeof(byte)) return Result.CastValueFrom(ReadByte(address)); - if (dataType == typeof(short)) return Result.CastValueFrom(ReadShort(address)); - if (dataType == typeof(ushort)) return Result.CastValueFrom(ReadUShort(address)); - if (dataType == typeof(int)) return Result.CastValueFrom(ReadInt(address)); - if (dataType == typeof(uint)) return Result.CastValueFrom(ReadUInt(address)); - if (dataType == typeof(IntPtr)) return Result.CastValueFrom(ReadIntPtr(address)); - if (dataType == typeof(float)) return Result.CastValueFrom(ReadFloat(address)); - if (dataType == typeof(long)) return Result.CastValueFrom(ReadLong(address)); - if (dataType == typeof(ulong)) return Result.CastValueFrom(ReadULong(address)); - if (dataType == typeof(double)) return Result.CastValueFrom(ReadDouble(address)); + // Check that the type is not a reference type + if (!type.IsValueType) + return new ReadFailureOnUnsupportedType(type); + + // Check the address + if (address == UIntPtr.Zero) + return new ReadFailureOnZeroPointer(); + if (!IsBitnessCompatible(address)) + return new ReadFailureOnIncompatibleBitness(address); + + // Get the size of the target type + int size = Marshal.SizeOf(type); - // Not a primitive type. Try to read it as a structure. - // To do that, we first have to determine the size of the structure. - int size = Marshal.SizeOf(dataType); + // Read the bytes from the process memory + var readResult = _osService.ReadProcessMemory(_processHandle, address, (ulong)size); + if (!readResult.IsSuccess) + return new ReadFailureOnSystemRead(readResult.Error); - // Then we read the bytes from the process memory. - var bytesResult = ReadBytes(address, (ulong)size); - if (!bytesResult.IsSuccess) - return bytesResult; + // Convert the bytes into the target type + // Special case for booleans, which do not work well with Marshal.PtrToStructure + if (type == typeof(bool)) + return readResult.Value[0] != 0; - // We have the bytes. Now we need to convert them to the structure. // We cannot use MemoryMarshal.Read here because the data type is a variable, not a generic type. // So we have to use a GCHandle to pin the bytes in memory and then use Marshal.PtrToStructure. - var handle = GCHandle.Alloc(bytesResult.Value, GCHandleType.Pinned); + var handle = GCHandle.Alloc(readResult.Value, GCHandleType.Pinned); try { var pointer = handle.AddrOfPinnedObject(); - object? structure = Marshal.PtrToStructure(pointer, dataType); + object? structure = Marshal.PtrToStructure(pointer, type); if (structure == null) return new ReadFailureOnConversionFailure(); return structure; @@ -103,266 +133,6 @@ public Result Read(Type dataType, UIntPtr address) } } - /// - /// Reads a boolean from the address referred by the given pointer path, in the process memory. - /// - /// Optimized, reusable path to the target address. - /// The value read from the process memory, or a read failure. - public Result ReadBool(PointerPath pointerPath) - { - var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadBool(addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); - } - - /// - /// Reads a boolean from the given address in the process memory. - /// - /// Target address in the process memory. - /// The value read from the process memory, or a read failure. - public Result ReadBool(UIntPtr address) - { - var bytesResult = ReadBytes(address, 1); - if (bytesResult.IsFailure) - return bytesResult.Error; - return BitConverter.ToBoolean(bytesResult.Value); - } - - /// - /// Reads a byte from the address referred by the given pointer path, in the process memory. - /// - /// Optimized, reusable path to the target address. - /// The value read from the process memory, or a read failure. - public Result ReadByte(PointerPath pointerPath) - { - var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadByte(addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); - } - - /// - /// Reads a byte from the given address in the process memory. - /// - /// Target address in the process memory. - /// The value read from the process memory, or a read failure. - public Result ReadByte(UIntPtr address) - { - var bytesResult = ReadBytes(address, 1); - return bytesResult.IsSuccess ? bytesResult.Value[0] : bytesResult.Error; - } - - /// - /// Reads a short from the address referred by the given pointer path, in the process memory. - /// - /// Optimized, reusable path to the target address. - /// The value read from the process memory, or a read failure. - public Result ReadShort(PointerPath pointerPath) - { - var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadShort(addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); - } - - /// - /// Reads a short from the given address in the process memory. - /// - /// Target address in the process memory. - /// The value read from the process memory, or a read failure. - public Result ReadShort(UIntPtr address) - { - var bytesResult = ReadBytes(address, 2); - return bytesResult.IsSuccess ? BitConverter.ToInt16(bytesResult.Value) : bytesResult.Error; - } - - /// - /// Reads an unsigned short from the address referred by the given pointer path, in the process memory. - /// - /// Optimized, reusable path to the target address. - /// The value read from the process memory, or a read failure. - public Result ReadUShort(PointerPath pointerPath) - { - var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadUShort(addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); - } - - /// - /// Reads an unsigned short from the given address in the process memory. - /// - /// Target address in the process memory. - /// The value read from the process memory, or a read failure. - public Result ReadUShort(UIntPtr address) - { - var bytesResult = ReadBytes(address, 2); - return bytesResult.IsSuccess ? BitConverter.ToUInt16(bytesResult.Value) : bytesResult.Error; - } - - /// - /// Reads an integer from the address referred by the given pointer path, in the process memory. - /// - /// Optimized, reusable path to the target address. - /// The value read from the process memory, or a read failure. - public Result ReadInt(PointerPath pointerPath) - { - var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadInt(addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); - } - - /// - /// Reads an integer from the given address in the process memory. - /// - /// Target address in the process memory. - /// The value read from the process memory, or a read failure. - public Result ReadInt(UIntPtr address) - { - var bytesResult = ReadBytes(address, 4); - return bytesResult.IsSuccess ? BitConverter.ToInt32(bytesResult.Value) : bytesResult.Error; - } - - /// - /// Reads an unsigned integer from the address referred by the given pointer path, in the process memory. - /// - /// Optimized, reusable path to the target address. - /// The value read from the process memory, or a read failure. - public Result ReadUInt(PointerPath pointerPath) - { - var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadUInt(addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); - } - - /// - /// Reads an unsigned integer from the given address in the process memory. - /// - /// Target address in the process memory. - /// The value read from the process memory, or a read failure. - public Result ReadUInt(UIntPtr address) - { - var bytesResult = ReadBytes(address, 4); - return bytesResult.IsSuccess ? BitConverter.ToUInt32(bytesResult.Value) : bytesResult.Error; - } - - /// - /// Reads a pointer from the address referred by the given pointer path, in the process memory. - /// - /// Optimized, reusable path to the target address. - /// The value read from the process memory, or a read failure. - public Result ReadIntPtr(PointerPath pointerPath) - { - var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadIntPtr(addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); - } - - /// - /// Reads a pointer from the given address in the process memory. - /// - /// Target address in the process memory. - /// The value read from the process memory, or a read failure. - public Result ReadIntPtr(UIntPtr address) - { - var bytesResult = ReadBytes(address, (ulong)IntPtr.Size); - if (bytesResult.IsFailure) - return bytesResult.Error; - - byte[] bytes = bytesResult.Value; - return _is64Bits ? (UIntPtr)BitConverter.ToUInt64(bytes) - : (UIntPtr)BitConverter.ToUInt32(bytes); - } - - /// - /// Reads a float from the address referred by the given pointer path, in the process memory. - /// - /// Optimized, reusable path to the target address. - /// The value read from the process memory, or a read failure. - public Result ReadFloat(PointerPath pointerPath) - { - var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadFloat(addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); - } - - /// - /// Reads a float from the given address in the process memory. - /// - /// Target address in the process memory. - /// The value read from the process memory, or a read failure. - public Result ReadFloat(UIntPtr address) - { - var bytesResult = ReadBytes(address, 4); - return bytesResult.IsSuccess ? BitConverter.ToSingle(bytesResult.Value) : bytesResult.Error; - } - - /// - /// Reads a long from the address referred by the given pointer path, in the process memory. - /// - /// Optimized, reusable path to the target address. - /// The value read from the process memory, or a read failure. - public Result ReadLong(PointerPath pointerPath) - { - var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadLong(addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); - } - - /// - /// Reads a long from the given address in the process memory. - /// - /// Target address in the process memory. - /// The value read from the process memory, or a read failure. - public Result ReadLong(UIntPtr address) - { - var bytesResult = ReadBytes(address, 8); - return bytesResult.IsSuccess ? BitConverter.ToInt64(bytesResult.Value) : bytesResult.Error; - } - - /// - /// Reads an unsigned long from the address referred by the given pointer path, in the process memory. - /// - /// Optimized, reusable path to the target address. - /// The value read from the process memory, or a read failure. - public Result ReadULong(PointerPath pointerPath) - { - var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadULong(addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); - } - - /// - /// Reads an unsigned long from the given address in the process memory. - /// - /// Target address in the process memory. - /// The value read from the process memory, or a read failure. - public Result ReadULong(UIntPtr address) - { - var bytesResult = ReadBytes(address, 8); - return bytesResult.IsSuccess ? BitConverter.ToUInt64(bytesResult.Value) : bytesResult.Error; - } - - /// - /// Reads a double from the address referred by the given pointer path, in the process memory. - /// - /// Optimized, reusable path to the target address. - /// The value read from the process memory, or a read failure. - public Result ReadDouble(PointerPath pointerPath) - { - var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadDouble(addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); - } - - /// - /// Reads a double from the given address in the process memory. - /// - /// Target address in the process memory. - /// The value read from the process memory, or a read failure. - public Result ReadDouble(UIntPtr address) - { - var bytesResult = ReadBytes(address, 8); - return bytesResult.IsSuccess ? BitConverter.ToDouble(bytesResult.Value) : bytesResult.Error; - } - /// /// Reads a string from the address referred by the given pointer path, in the process memory. /// Optionally uses the given string settings and restricts the string to the specified max size. diff --git a/src/MindControl/Results/ReadFailure.cs b/src/MindControl/Results/ReadFailure.cs index 473b4c8..72e7091 100644 --- a/src/MindControl/Results/ReadFailure.cs +++ b/src/MindControl/Results/ReadFailure.cs @@ -28,7 +28,12 @@ public enum ReadFailureReason /// /// Failure when trying to convert the bytes read from memory to the target type. /// - ConversionFailure + ConversionFailure, + + /// + /// The type to read is not supported. + /// + UnsupportedType } /// @@ -98,6 +103,17 @@ public record ReadFailureOnConversionFailure() { /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() - => "Failed to convert the bytes read from memory to the target type. Try using primitive types or structure types."; + public override string ToString() => "Failed to convert the bytes read from memory to the target type. Check that the type does not contain references or pointers."; +} + +/// +/// Represents a failure in a memory read operation when the type to read is not supported. +/// +/// Type that caused the failure. +public record ReadFailureOnUnsupportedType(Type ProvidedType) + : ReadFailure(ReadFailureReason.UnsupportedType) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The type {ProvidedType} is not supported. Reference types are not supported."; } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index 917d4ad..2d0eaca 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -11,6 +11,9 @@ namespace MindControl.Test.ProcessMemoryTests; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ProcessMemoryReadTest : ProcessMemoryTest { + /// A struct with a couple fields, to test reading structs. + public record struct TestStruct(double A, int B); + /// /// Tests . /// Reads a first time after initialization, and then a second time after value are modified. @@ -43,118 +46,206 @@ public void ReadTwoStepGenericTest(Type targetType, int pointerOffset, object? e } /// - /// Tests . - /// Reads a known boolean from the target process after initialization. - /// It should be equal to its known value. + /// Tests . + /// Reads a known boolean from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. /// [Test] public void ReadBoolTest() - => Assert.That(TestProcessMemory!.ReadBool(OuterClassPointer + 0x48).GetValueOrDefault(), Is.EqualTo(true)); - + { + var address = OuterClassPointer + 0x48; + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(true)); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(false)); + } + /// - /// Tests . - /// Reads a known byte from the target process after initialization. - /// It should be equal to its known value. + /// Tests . + /// Reads a known byte from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. /// [Test] public void ReadByteTest() - => Assert.That(TestProcessMemory!.ReadByte(OuterClassPointer + 0x49).GetValueOrDefault(), Is.EqualTo(0xAC)); - + { + var address = OuterClassPointer + 0x49; + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(0xAC)); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(0xDC)); + } + /// - /// Tests . - /// Reads a known short from the target process after initialization. - /// It should be equal to its known value. + /// Tests . + /// Reads a known short from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. /// [Test] - public void ReadShortTest() => Assert.That( - TestProcessMemory!.ReadShort(OuterClassPointer + 0x44).GetValueOrDefault(), Is.EqualTo(-7777)); - + public void ReadShortTest() + { + var address = OuterClassPointer + 0x44; + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(-7777)); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(-8888)); + } + /// - /// Tests . - /// Reads a known unsigned short from the target process after initialization. - /// It should be equal to its known value. + /// Tests . + /// Reads a known unsigned short from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. /// [Test] - public void ReadUShortTest() => Assert.That( - TestProcessMemory!.ReadUShort(OuterClassPointer + 0x46).GetValueOrDefault(), Is.EqualTo(8888)); - + public void ReadUShortTest() + { + var address = OuterClassPointer + 0x46; + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(8888)); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(9999)); + } + /// - /// Tests . - /// Reads a known integer from the target process after initialization. - /// It should be equal to its known value. + /// Tests . + /// Reads a known integer from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. /// [Test] public void ReadIntTest() - => Assert.That(TestProcessMemory!.ReadInt(OuterClassPointer + 0x38).GetValueOrDefault(), Is.EqualTo(-7651)); - + { + var address = OuterClassPointer + 0x38; + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(-7651)); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(987411)); + } + /// - /// Tests . - /// Reads a known unsigned integer from the target process after initialization. - /// It should be equal to its known value. + /// Tests . + /// Reads a known unsigned integer from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. /// [Test] - public void ReadUIntTest() => Assert.That( - TestProcessMemory!.ReadUInt(OuterClassPointer + 0x3C).GetValueOrDefault(), Is.EqualTo(6781631)); - + public void ReadUIntTest() + { + var address = OuterClassPointer + 0x3C; + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(6781631)); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(444763)); + } + /// - /// Tests . - /// Reads a known long from the target process after initialization. - /// It should be equal to its known value. + /// Tests . + /// Reads a known long from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. /// [Test] - public void ReadLongTest() => Assert.That( - TestProcessMemory!.ReadLong(OuterClassPointer + 0x20).GetValueOrDefault(), Is.EqualTo(-65746876815103L)); - + public void ReadLongTest() + { + var address = OuterClassPointer + 0x20; + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(-65746876815103L)); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(-777654646516513L)); + } + /// - /// Tests . - /// Reads a known unsigned long from the target process after initialization. - /// It should be equal to its known value. + /// Tests . + /// Reads a known unsigned long from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. /// [Test] - public void ReadULongTest() => Assert.That( - TestProcessMemory!.ReadULong(OuterClassPointer + 0x28).GetValueOrDefault(), Is.EqualTo(76354111324644L)); - + public void ReadULongTest() + { + var address = OuterClassPointer + 0x28; + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(76354111324644L)); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(34411111111164L)); + } + + /// + /// Tests . + /// Reads a known float from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. + /// + [Test] + public void ReadFloatTest() + { + var address = OuterClassPointer + 0x40; + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(3456765.323f)); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(-123444.147f)); + } + /// - /// Tests . - /// Reads a known float from the target process after initialization. - /// It should be equal to its known value. + /// Tests . + /// Reads a known double from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. /// [Test] - public void ReadFloatTest() => Assert.That( - TestProcessMemory!.ReadFloat(OuterClassPointer + 0x40).GetValueOrDefault(), Is.EqualTo(3456765.323f)); + public void ReadDoubleTest() + { + var address = OuterClassPointer + 0x30; + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(79879131651.33345)); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(-99879416311.4478)); + } /// - /// Tests . - /// Reads a known double from the target process after initialization. - /// It should be equal to its known value. + /// Tests . + /// Reads a known struct from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. /// [Test] - public void ReadDoubleTest() => Assert.That( - TestProcessMemory!.ReadDouble(OuterClassPointer + 0x30).GetValueOrDefault(), Is.EqualTo(79879131651.33345)); + public void ReadStructureTest() + { + var pointerPath = new PointerPath($"{OuterClassPointer:X}+30"); + Assert.That(TestProcessMemory!.Read(pointerPath).Value, + Is.EqualTo(new TestStruct(79879131651.33345, -7651))); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(pointerPath).Value, + Is.EqualTo(new TestStruct(-99879416311.4478, 987411))); + } /// - /// Tests . - /// Reads a known byte array from the target process after initialization. - /// It should be equal to its known value. + /// Tests . + /// Reads a known struct from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. /// [Test] - public void ReadBytesTest() => Assert.That( - TestProcessMemory!.ReadBytes($"{OuterClassPointer:X}+18,10", 4).GetValueOrDefault(), - Is.EqualTo(new byte[] { 0x11, 0x22, 0x33, 0x44 })); + public void ReadStructureAsObjectTest() + { + var pointerPath = new PointerPath($"{OuterClassPointer:X}+30"); + Assert.That(TestProcessMemory!.Read(typeof(TestStruct), pointerPath).Value, + Is.EqualTo(new TestStruct(79879131651.33345, -7651))); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(typeof(TestStruct), pointerPath).Value, + Is.EqualTo(new TestStruct(-99879416311.4478, 987411))); + } /// - /// Tests . - /// Reads a known long from the target process after initialization. + /// Tests . + /// Reads a known byte array from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. + /// + [Test] + public void ReadBytesTest() + { + PointerPath pointerPath = $"{OuterClassPointer:X}+18,10"; + Assert.That(TestProcessMemory!.ReadBytes(pointerPath, 4).GetValueOrDefault(), + Is.EqualTo(new byte[] { 0x11, 0x22, 0x33, 0x44 })); + ProceedToNextStep(); + Assert.That(TestProcessMemory.ReadBytes(pointerPath, 4).GetValueOrDefault(), + Is.EqualTo(new byte[] { 0x55, 0x66, 0x77, 0x88 })); + } + + /// + /// Tests . + /// Reads a known long from the target process before and after the process changes their value. /// This method uses a and reads a value that is nested in the known instance. - /// It should be equal to its known value. + /// It should be equal to its known values before and after modification. /// [Test] public void ReadNestedLongTest() => Assert.That( - TestProcessMemory!.ReadLong($"{OuterClassPointer:X}+10,8").GetValueOrDefault(), Is.EqualTo(999999999999L)); + TestProcessMemory!.Read($"{OuterClassPointer:X}+10,8").GetValueOrDefault(), Is.EqualTo(999999999999L)); /// - /// Tests . - /// Reads an ulong with the max value from the target process after initialization. + /// Tests . + /// Reads an ulong with the max value from the target process before and after the process changes their value. /// This method uses a and reads a value that is nested in the known instance. /// Reading this value as a pointer should yield a valid pointer. This test validates that there is no arithmetic /// overflow caused by signed pointer usage. @@ -162,25 +253,26 @@ public void ReadNestedLongTest() => Assert.That( [Test] public void ReadUIntPtrMaxValueTest() { - var ptr = TestProcessMemory!.ReadIntPtr($"{OuterClassPointer:X}+10,10").GetValueOrDefault(); + var ptr = TestProcessMemory!.Read($"{OuterClassPointer:X}+10,10").GetValueOrDefault(); Assert.That(ptr.ToUInt64(), Is.EqualTo(ulong.MaxValue)); } /// /// Tests an edge case where the pointer path given to a read operation points to the last possible byte in memory /// (the maximum value of a UIntPtr). - /// The read operation is expected to fail (this memory region is not valid). + /// The read operation is expected to fail with a (this memory region is not + /// readable). /// [Test] public void ReadAtMaxPointerValueTest() { - var result = TestProcessMemory!.ReadByte($"{OuterClassPointer:X}+10,10,0"); + var result = TestProcessMemory!.Read($"{OuterClassPointer:X}+10,10,0"); Assert.That(result.IsSuccess, Is.False); var error = result.Error; Assert.That(error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); var systemError = ((ReadFailureOnSystemRead)error).SystemReadFailure; Assert.That(systemError, Is.TypeOf(typeof(OperatingSystemCallFailure))); - var osError = ((OperatingSystemCallFailure)systemError); + var osError = (OperatingSystemCallFailure)systemError; Assert.That(osError.ErrorCode, Is.GreaterThan(0)); Assert.That(osError.ErrorMessage, Is.Not.Empty); } @@ -193,7 +285,7 @@ public void ReadAtMaxPointerValueTest() [Test] public void ReadOverMaxPointerValueTest() { - var result = TestProcessMemory!.ReadByte($"{OuterClassPointer:X}+10,10,1"); + var result = TestProcessMemory!.Read($"{OuterClassPointer:X}+10,10,1"); Assert.That(result.IsSuccess, Is.False); var error = result.Error; Assert.That(error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); @@ -204,13 +296,41 @@ public void ReadOverMaxPointerValueTest() Assert.That(outOfRangeError.PreviousAddress, Is.Not.Null); Assert.That(outOfRangeError.PreviousAddress, Is.Not.EqualTo(UIntPtr.Zero)); } + + /// + /// Tests the method with a reference type. + /// It should fail with a . + /// + [Test] + public void ReadIncompatibleTypeTest() + { + var result = TestProcessMemory!.Read(typeof(string), OuterClassPointer + 0x38); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnUnsupportedType))); + } + + /// Structure used to test reading a structure with references. + private record struct TestStructWithReferences(int A, string B); + + /// + /// Tests the method with a structure type that contains reference + /// types. + /// It should fail with a . + /// + [Test] + public void ReadStructureWithReferencesTest() + { + var result = TestProcessMemory!.Read(OuterClassPointer + 0x38); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnConversionFailure))); + } /// /// Tests . /// Reads a known string from the target process after initialization, without using any parameter beyond the /// pointer path. /// This method uses a to read the string at the right position. - /// It should be equal to its known value. + /// It should be equal to its known values before and after modification. /// [Test] public void ReadStringWithNoParametersTest() From 04d803fa1d29c35f03dbdf99596ff13c9b1f1e39 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Tue, 21 May 2024 21:17:23 +0200 Subject: [PATCH 09/66] Added a benchmark project to test and compare with Memory.dll --- MindControl.sln | 10 +- test/MindControl.Benchmark/Benchmarks.cs | 91 +++++++++++++++++++ .../MindControl.Benchmark.csproj | 24 +++++ test/MindControl.Benchmark/Program.cs | 9 ++ 4 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 test/MindControl.Benchmark/Benchmarks.cs create mode 100644 test/MindControl.Benchmark/MindControl.Benchmark.csproj create mode 100644 test/MindControl.Benchmark/Program.cs diff --git a/MindControl.sln b/MindControl.sln index 9ede659..6aa1161 100644 --- a/MindControl.sln +++ b/MindControl.sln @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MindControl.Test.TargetApp" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MindControl.Test.InjectedLibrary", "test\MindControl.Test.InjectedLibrary\MindControl.Test.InjectedLibrary.vcxproj", "{AA8C9AF6-7C31-43D1-A519-DBB1C3252127}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindControl.Benchmark", "test\MindControl.Benchmark\MindControl.Benchmark.csproj", "{5C1B693D-D176-41B2-A47A-E78E098171CB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,8 +33,12 @@ Global {9FE5EF8E-1230-42DB-A6B2-0C2633D32A1F}.Release|Any CPU.Build.0 = Release|Any CPU {AA8C9AF6-7C31-43D1-A519-DBB1C3252127}.Debug|Any CPU.ActiveCfg = Debug|x64 {AA8C9AF6-7C31-43D1-A519-DBB1C3252127}.Debug|Any CPU.Build.0 = Debug|x64 - {AA8C9AF6-7C31-43D1-A519-DBB1C3252127}.Release|Any CPU.ActiveCfg = Release|Win32 - {AA8C9AF6-7C31-43D1-A519-DBB1C3252127}.Release|Any CPU.Build.0 = Release|Win32 + {AA8C9AF6-7C31-43D1-A519-DBB1C3252127}.Release|Any CPU.ActiveCfg = Release|x64 + {AA8C9AF6-7C31-43D1-A519-DBB1C3252127}.Release|Any CPU.Build.0 = Release|x64 + {5C1B693D-D176-41B2-A47A-E78E098171CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C1B693D-D176-41B2-A47A-E78E098171CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C1B693D-D176-41B2-A47A-E78E098171CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C1B693D-D176-41B2-A47A-E78E098171CB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/test/MindControl.Benchmark/Benchmarks.cs b/test/MindControl.Benchmark/Benchmarks.cs new file mode 100644 index 0000000..f576475 --- /dev/null +++ b/test/MindControl.Benchmark/Benchmarks.cs @@ -0,0 +1,91 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Text; +using System.Threading; +using BenchmarkDotNet.Attributes; +using Memory; + +namespace MindControl.Benchmark; + +[MemoryDiagnoser] +public class Benchmarks +{ + private Process _targetProcess; + private ProcessMemory _processMemory; + private UIntPtr _outerClassPointer; + private Mem _mem; + + [GlobalSetup] + public void GlobalSetup() + { + _targetProcess = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "./MindControl.Test.TargetApp.exe", + RedirectStandardOutput = true, + RedirectStandardInput = true, + StandardOutputEncoding = Encoding.UTF8 + } + }; + _targetProcess.Start(); + + string line = _targetProcess.StandardOutput.ReadLine(); + if (!UIntPtr.TryParse(line, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var outerClassPointer)) + throw new Exception($"Could not read the outer class pointer output by the app: \"{line}\"."); + + _outerClassPointer = outerClassPointer; + _processMemory = ProcessMemory.OpenProcessById(_targetProcess.Id); + _mem = new Mem(); + _mem.OpenProcess(_targetProcess.Id); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + _processMemory.Dispose(); + _targetProcess.Kill(); + + // Make sure the process is exited before going on + Thread.Sleep(250); + } + + [Benchmark(Description = "MindControl Read")] + public int MindControlReadGenericType() + { + var result = _processMemory.Read(_outerClassPointer + 0x38).Value; + if (result != -7651) + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "MindControl Read(Type)")] + public int MindControlReadObject() + { + var result = (int)_processMemory.Read(typeof(int), _outerClassPointer + 0x38).Value; + if (result != -7651) + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "Memory.dll ReadInt")] + public int MemoryReadInt() + { + var address = _outerClassPointer + 0x38; + var result = _mem.ReadInt(address.ToString("X")); + if (result != -7651) + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "Memory.dll ReadMemory")] + public int MemoryReadGeneric() + { + var address = _outerClassPointer + 0x38; + var result = _mem.ReadMemory(address.ToString("X")); + if (result != -7651) + throw new Exception("Unexpected result"); + return result; + } +} \ No newline at end of file diff --git a/test/MindControl.Benchmark/MindControl.Benchmark.csproj b/test/MindControl.Benchmark/MindControl.Benchmark.csproj new file mode 100644 index 0000000..ce784a0 --- /dev/null +++ b/test/MindControl.Benchmark/MindControl.Benchmark.csproj @@ -0,0 +1,24 @@ + + + net8.0 + Exe + + + AnyCPU + pdbonly + true + true + true + Release + false + + + + + + + + + + + \ No newline at end of file diff --git a/test/MindControl.Benchmark/Program.cs b/test/MindControl.Benchmark/Program.cs new file mode 100644 index 0000000..f1c6a95 --- /dev/null +++ b/test/MindControl.Benchmark/Program.cs @@ -0,0 +1,9 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Running; +using MindControl.Benchmark; + +var config = DefaultConfig.Instance + // We have to disable the optimizations validator because Memory.dll is not optimized + // But we should always run benchmarks in Release in any case. + .WithOptions(ConfigOptions.DisableOptimizationsValidator); +BenchmarkRunner.Run(config, args); From f1baa70593dfb14e68d54b44ceaaeb4005f818a7 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Fri, 24 May 2024 16:34:56 +0200 Subject: [PATCH 10/66] Optimized PointerPath parsing --- src/MindControl/Addressing/PointerOffset.cs | 106 +++++ src/MindControl/Addressing/PointerPath.cs | 383 ++++++++++-------- .../ProcessMemory/ProcessMemory.Addressing.cs | 29 +- .../ProcessMemory/ProcessMemory.Read.cs | 12 +- .../Results/PathEvaluationFailure.cs | 13 +- .../BenchmarkMemorySetup.cs | 81 ++++ test/MindControl.Benchmark/Benchmarks.cs | 91 ----- .../PointerPathBenchmark.cs | 16 + test/MindControl.Benchmark/Program.cs | 4 +- .../ReadIntByAddressBenchmark.cs | 55 +++ .../ReadLongByPointerPathBenchmark.cs | 78 ++++ .../AddressingTests/PointerPathTest.cs | 86 ++-- .../ProcessMemoryEvaluateTest.cs | 11 +- .../ProcessMemoryReadTest.cs | 8 +- 14 files changed, 629 insertions(+), 344 deletions(-) create mode 100644 src/MindControl/Addressing/PointerOffset.cs create mode 100644 test/MindControl.Benchmark/BenchmarkMemorySetup.cs delete mode 100644 test/MindControl.Benchmark/Benchmarks.cs create mode 100644 test/MindControl.Benchmark/PointerPathBenchmark.cs create mode 100644 test/MindControl.Benchmark/ReadIntByAddressBenchmark.cs create mode 100644 test/MindControl.Benchmark/ReadLongByPointerPathBenchmark.cs diff --git a/src/MindControl/Addressing/PointerOffset.cs b/src/MindControl/Addressing/PointerOffset.cs new file mode 100644 index 0000000..da76a81 --- /dev/null +++ b/src/MindControl/Addressing/PointerOffset.cs @@ -0,0 +1,106 @@ +namespace MindControl; + +/// +/// Describes a pointer offset as part of a . +/// Effectively, this is a number ranging from -0xFFFFFFFFFFFFFFFF to 0xFFFFFFFFFFFFFFFF. +/// +/// Absolute value of the offset. +/// Sign of the offset. +public readonly record struct PointerOffset(ulong Offset, bool IsNegative) +{ + /// + /// Represents a zero offset. + /// + public static readonly PointerOffset Zero = new(0, false); + + /// + /// Gets a value indicating whether this offset is 64-bits. + /// + public bool Is64Bits => Offset > uint.MaxValue; + + /// + /// Produces the result of the addition between this offset and the given value. + /// + /// Value to add to this offset. + /// Sign of the value to add. + /// The result of the addition, or null if the result overflows or underflows. + public PointerOffset? Plus(ulong value, bool isNegative) + { + if (IsNegative == isNegative) + { + // This handles both overflow (value going above 0xFFFFFFFFFFFFFFFF) and underflow (value going below + // -0xFFFFFFFFFFFFFFFF) + if (Offset > ulong.MaxValue - value) + return null; + + return new(Offset + value, IsNegative); + } + + // If the signs are different, we need to subtract the smaller from the larger number. + // The sign of the result will be the sign of the larger number. + if (Offset > value) + return new(Offset - value, IsNegative); + if (Offset == value) + return Zero; + return new(value - Offset, isNegative); + } + + /// + /// Produces the result of the addition between this offset and the given offset. + /// + /// Offset to add to this offset. + /// The result of the addition, or null if the result overflows or underflows. + public PointerOffset? Plus(PointerOffset other) + => Plus(other.Offset, other.IsNegative); + + /// + /// Offsets the given address by this offset. + /// + /// Address to offset. + /// The offset address, or null if the result overflows or is negative. + public UIntPtr? OffsetAddress(UIntPtr address) + { + var sum = Plus((ulong)address, false); + if (sum == null || sum.Value.IsNegative || sum.Value.Offset > (ulong)UIntPtr.MaxValue) + return null; + + return (UIntPtr)sum.Value.Offset; + } + + /// + /// Returns the value of this offset as an address, or null if the offset is negative. + /// + /// The value of this offset as an address, or null if the offset is negative. + public UIntPtr? AsAddress() + { + if (IsNegative || Offset > (ulong)UIntPtr.MaxValue) + return null; + return (UIntPtr)Offset; + } + + /// + /// Produces the result of the sum of the multiplication of the value of this offset by 16 and the given value. + /// The resulting sign will always be the sign of this offset. + /// This is useful when building up a pointer offset from a sequence of bytes. + /// + /// Value to add to the result of the multiplication. + /// The result of the multiplication and addition, or null if the result overflows or underflows. + public PointerOffset? ShiftAndAdd(byte value) + { + // Check if multiplying by 16 will overflow + if (Offset > ulong.MaxValue / 16) + return null; + + ulong shiftedOffset = Offset * 16; + + // Check if adding the value will overflow + if (shiftedOffset > ulong.MaxValue - value) + return null; + + return new(shiftedOffset + value, IsNegative); + } + + /// Returns the fully qualified type name of this instance. + /// The fully qualified type name. + public override string ToString() => IsNegative ? $"-{Offset:X}" : $"{Offset:X}"; +} \ No newline at end of file diff --git a/src/MindControl/Addressing/PointerPath.cs b/src/MindControl/Addressing/PointerPath.cs index 0c976f0..de4cc00 100644 --- a/src/MindControl/Addressing/PointerPath.cs +++ b/src/MindControl/Addressing/PointerPath.cs @@ -1,8 +1,4 @@ -using System.Globalization; -using System.Numerics; -using System.Text.RegularExpressions; - -namespace MindControl; +namespace MindControl; /// /// Holds a string expression consisting in a base address followed by a sequence of pointer offsets. @@ -15,7 +11,7 @@ public class PointerPath /// /// Stores data parsed and computed internally from a pointer path expression. /// - private struct ExpressionParsedData + private readonly struct ExpressionParsedData { /// /// Gets the base module name. @@ -29,22 +25,19 @@ private struct ExpressionParsedData /// For example, for the expression "myprocess.exe+01F4684-4,18+4,C", gets 0x1F4680. /// For expressions without a module offset, like "01F4684-4,18" or "myprocess.exe", gets 0. /// - public BigInteger BaseModuleOffset { get; init; } + public PointerOffset BaseModuleOffset { get; init; } /// /// Gets the collection of pointer offsets to follow sequentially in order to evaluate the memory address. /// For example, for the expression "myprocess.exe+01F4684-4,18+4,C", gets [0x1C, 0xC]. /// - public BigInteger[] PointerOffsets { get; init; } + public PointerOffset[] PointerOffsets { get; init; } /// - /// Gets a boolean indicating if the address is a 64-bit only address, or if it can also be used in a 32-bits + /// Gets a boolean indicating if the path is a 64-bit only path, or if it can also be used in a 32-bits /// process. - /// For example, for the expression "app.dll+0000F04AA1218410", this boolean would be True. - /// For the expression "app.dll+00000000F04AA121", this boolean would be False. - /// Note that evaluating a 32-bit-compatible address may still end up overflowing. /// - public bool IsStrictly64Bits { get; init; } + public bool IsStrictly64Bits => BaseModuleOffset.Is64Bits || PointerOffsets.Any(offset => offset.Is64Bits); } /// @@ -67,16 +60,16 @@ private struct ExpressionParsedData /// For example, for the expression "myprocess.exe+01F4684-4,18+4,C", gets 0x1F4680. /// For expressions without a module offset, like "01F4684-4,18" or "myprocess.exe", gets 0. /// - public BigInteger BaseModuleOffset => _parsedData.BaseModuleOffset; + public PointerOffset BaseModuleOffset => _parsedData.BaseModuleOffset; /// /// Gets the collection of pointer offsets to follow sequentially in order to evaluate the memory address. /// For example, for the expression "myprocess.exe+01F4684-4,18+4,C", gets [0x1C, 0xC]. /// - public BigInteger[] PointerOffsets => _parsedData.PointerOffsets; + public PointerOffset[] PointerOffsets => _parsedData.PointerOffsets; /// - /// Gets a boolean indicating if the address is a 64-bit only address, or if it can also be used in a 32-bits + /// Gets a boolean indicating if the path is a 64-bit only path, or if it can also be used in a 32-bits /// process. /// For example, for the expression "app.dll+0000F04AA1218410", this boolean would be True. /// For the expression "app.dll+00000000F04AA121", this boolean would be False. @@ -142,7 +135,7 @@ public static bool IsValid(string expression, bool allowOnly32Bits = false) return parsedData == null || allowOnly32Bits && parsedData.Value.IsStrictly64Bits ? null : new PointerPath(expression, parsedData); } - + /// /// Attempts to parse the given expression. Returns the parsed data container when successful, or null if the /// expression is not valid. @@ -151,199 +144,251 @@ public static bool IsValid(string expression, bool allowOnly32Bits = false) /// the parsed data container when successful, or null if the expression is not valid. private static ExpressionParsedData? Parse(string expression) { - if (string.IsNullOrWhiteSpace(expression)) - return null; - - var baseModuleName = ParseBaseModuleName(expression); - var pointerOffsets = ParsePointerOffsets(expression, baseModuleName != null); + // A note about the parsing code: + // It is designed to be fast and to allocate the strict minimum amount of memory. + // This means we cannot use regular expressions, splits, trims, lists, etc. + // That makes the code significantly harder to read and maintain, but this is a potential hot path and so it is + // worth the compromise. - // If pointer offsets cannot be parsed, it means the expression is not valid and so we cannot parse. - if (pointerOffsets == null) + // Quick checks to avoid parsing the expression if it is obviously invalid + // The expression must not be empty, and must not end with a comma or an operator + if (expression.Length == 0 || expression[^1] is ',' or '+' or '-') return null; - // If there is no base module name, the base address cannot be negative (e.g. "-1F,4"). - if (baseModuleName == null && pointerOffsets.Length > 0 && pointerOffsets[0] < 0) + // Try to read a module name from the expression + // If the value is null, the expression is invalid and we can straight up return null + // If there is no module name, the value will be an empty string + string? baseModuleName = ParseModuleName(expression, out int readCount); + if (baseModuleName == null) return null; - - var moduleOffset = baseModuleName != null ? ParseFirstStaticOffset(expression) : 0; - // If the base module offset expression is invalid, we cannot parse the expression. - if (moduleOffset == null) - return null; - - bool is64Bits = pointerOffsets.Append(moduleOffset.Value).Any(o => BigInteger.Abs(o) > uint.MaxValue); + bool hasModule = baseModuleName.Length > 0; + + // If the whole expression was the module name, we can return the parsed data + if (hasModule && readCount == expression.Length) + { + return new ExpressionParsedData + { + BaseModuleName = baseModuleName, + BaseModuleOffset = PointerOffset.Zero, + PointerOffsets = Array.Empty() + }; + } + + // Now we need to know if the module name has an offset (e.g. the "+4" in "myprocess.exe+4,2B") + // If the next character after the module name is a comma, it doesn't have an offset + bool hasModuleOffset = hasModule && expression[readCount] != ','; + if (hasModule && !hasModuleOffset) + readCount++; + + // Then, we determine how many offsets there are in the expression + // This is done by counting the number of commas in the expression + // The reason we need to know in advance is to allocate the array of offsets, to avoid the cost of a List + int offsetCount = hasModuleOffset ? 0 : 1; + for (int i = readCount; i < expression.Length; i++) + { + if (expression[i] == ',') + offsetCount++; + } + + var offsets = new PointerOffset[offsetCount]; + var index = readCount; + PointerOffset? baseModuleOffset = null; + int offsetIndex = 0; + while (index < expression.Length) + { + var offset = ParsePointerOffsetExpression(expression, index, out readCount); + + // If there is no base module, the first offset cannot be negative (we cannot start at a negative address) + if (offset == null || !hasModule && offsetIndex == 0 && offset.Value.IsNegative) + return null; + // If there is a base module offset, the first offset is the base module offset + if (hasModuleOffset && baseModuleOffset == null) + baseModuleOffset = offset; + else + offsets[offsetIndex++] = offset.Value; + + // Advance to the next expression + index += readCount; + } + return new ExpressionParsedData { - BaseModuleName = baseModuleName, - BaseModuleOffset = moduleOffset.Value, - PointerOffsets = pointerOffsets, - IsStrictly64Bits = is64Bits + BaseModuleName = hasModule ? baseModuleName : null, + BaseModuleOffset = baseModuleOffset ?? PointerOffset.Zero, + PointerOffsets = offsets }; } /// - /// Attempts to parse a base module name from the given expression. - /// For example, in "myapp.exe+1D,28", get "myapp.exe". - /// If the expression does not contain a base module name (e.g. "1F+6"), returns a null value. - /// Note that the definition of a base module name is very loose. All characters, including special characters and - /// numbers, are valid, with the exception of symbols used for other purposes in the pointer path syntax (,+-). + /// Parses the module name from the given expression. /// - /// Target expression to parse. - /// The base module name found in the input expression, or null if the expression does not contain a - /// module name. - private static string? ParseBaseModuleName(string expression) + /// Expression to parse. + /// Number of characters read from the expression. + /// The module name, or null if the expression is invalid. + private static string? ParseModuleName(string expression, out int readCount) { - // No module name in an empty expression. - if (string.IsNullOrWhiteSpace(expression)) + readCount = 0; + if (expression.Length == 0) return null; - // Take the first part of the first offset by splitting on "," and then on "+" or "-". - // Some examples: - // - For "myapp.exe+1D,28", get "myapp.exe" - // - For "1F284604+1D-4,1D", get "1F284604" - // - For "-1F+6F044C", get "" (because it starts with a sign). It can't be a module name so it's fine. - string firstOffset = expression.Split(',').First(); - string potentialModuleName = firstOffset.Split('-', '+').First(); - - // If the potential module name is empty, return null. - if (string.IsNullOrWhiteSpace(potentialModuleName)) - return null; + int startIndex = 0; + // Skip all whitespaces at the start of the expression + while (startIndex < expression.Length && char.IsWhiteSpace(expression[startIndex])) + startIndex++; - // Now we need to determine whether our potential module name is a real module name, or an offset/address. - // Attempt to parse it as a ulong. If it can't be parsed, it's a module name. - if (ulong.TryParse(potentialModuleName, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _)) - return null; - - // If we are here, we do have a module name. Format it and return it. - return potentialModuleName.Trim().Trim('"'); - } - - /// - /// In the given expression, retrieves the static offset value to add up with the first part of the first pointer - /// offset. This method is intended to parse the static offset of a module name. - /// For example, for the expression "myprocess.exe+1C-4,1B", the result would be 0x18 (0x1C-0x4 = 0x18). - /// If the expression contains no offset after the first part, the result will be 0. - /// If the offset expression is not valid, returns null. - /// - private static BigInteger? ParseFirstStaticOffset(string expression) - { - var firstSignOperatorIndex = expression.IndexOfAny(new[] { '+', '-' }, 1); - var firstPointerOperatorIndex = expression.IndexOf(','); - - // If there are no pointer operators in the expression, adjust the index to the length of the expression - if (firstPointerOperatorIndex == -1) - firstPointerOperatorIndex = expression.Length; + if (startIndex == expression.Length) + return null; // All whitespace - // No sign operator: no offset - if (firstSignOperatorIndex < 0) - return 0; - - // First sign operator is after the first pointer operator (e.g. "myprocess.exe,1C+4"): no offset - if (firstSignOperatorIndex > firstPointerOperatorIndex) - return 0; + // Determine if the module name is quoted. + // The rule is that the module name can optionally be quoted with double-quotes. + // In this case, the returned module name will be the content inside the quotes. + if (expression[startIndex] == '"') + { + // After skipping spaces, the first character is a quote + // This means the module name is quoted. Advance the start index to the first character after the quote. + startIndex++; + int endQuoteIndex = expression.IndexOf('"', startIndex); + if (endQuoteIndex == -1 || endQuoteIndex == startIndex + 1) + return null; // No closing quote or nothing inside the quotes: the expression is invalid + + readCount = endQuoteIndex + 1; + return expression.Substring(startIndex, endQuoteIndex - startIndex); + } + + // The module name is not quoted. If there is a module name, it will end either with a +/- operator, a ',', or + // the end of the string. + int endIndex = expression.IndexOfAny(new[] { '-', '+', ',' }); + if (endIndex == -1) + endIndex = expression.Length; + + // Remove all white spaces at the end of the module name + while (endIndex > startIndex && char.IsWhiteSpace(expression[endIndex - 1])) + endIndex--; - // Parse whatever is between the first sign operator and the first pointer operator - string offsetExpression = expression.Substring(firstSignOperatorIndex, - firstPointerOperatorIndex - firstSignOperatorIndex); - return ParsePointerOffsetExpression(offsetExpression); - } - - /// - /// In the given expression, retrieves and evaluates all pointer expressions (sequences of offsets separated by a - /// comma). For example, "18+4,C" would yield [0x1C, 0xC]. - /// - /// Expression to parse. - /// If True, the first pointer offset will be ignored because it contains a module name - /// and thus cannot be evaluated into a number. For example, "myapp.exe+8F,1C", would only return [0x1C]. - /// Pointer offsets evaluated as BigIntegers. - private static BigInteger[]? ParsePointerOffsets(string expression, bool hasModuleName) - { - if (string.IsNullOrWhiteSpace(expression)) - return null; + // If the endIndex was reduced to the startIndex, there is no module name + if (endIndex == startIndex) + return string.Empty; - var offsetExpressions = expression.Split(','); + int length = endIndex - startIndex; - var results = new List(); - // Skip the first split if the expression is known to have a module name. - foreach (var offsetExpression in offsetExpressions.Skip(hasModuleName ? 1 : 0)) + // We have to check if the module can be a valid hexadecimal address. + // If it is, what we read is an address and not a module, so we must return string.Empty. + PointerOffset? currentValue = PointerOffset.Zero; + for (int i = startIndex; i < endIndex; i++) { - // Parse each expression, and if any fails to parse, it means that the expression is not valid, and so - // we return null instead of the expected array. - var parsedOffset = ParsePointerOffsetExpression(offsetExpression); - if (parsedOffset == null) - return null; + char c = expression[i]; + if (char.IsWhiteSpace(c)) + continue; - results.Add(parsedOffset.Value); + byte hexadecimalValue = CharToValue(c); + if (hexadecimalValue == 255) + { + // Non-hexadecimal character + readCount = endIndex; + return expression.Substring(startIndex, length); + } + + currentValue = currentValue.Value.ShiftAndAdd(hexadecimalValue); + if (currentValue == null) + { + // Larger than the largest possible address + readCount = endIndex; + return expression.Substring(startIndex, length); + } } - return results.ToArray(); + return string.Empty; // The module name is a valid hexadecimal address } - + /// - /// Tries to parse the given pointer offset expression and returns the resulting value. - /// Because valid results range between -FFFFFFFFFFFFFFFF and FFFFFFFFFFFFFFFF, the return value is a BigInteger. - /// If the expression is invalid or results in a number that is outside of the valid range, returns null. + /// Parses a pointer offset expression (a sub-section that comes in-between ',' characters) from the given + /// expression. /// - /// Expression to parse. Example: "1F06+7C-4". - /// A BigInteger between -FFFFFFFFFFFFFFFF and FFFFFFFFFFFFFFFF representing the offset computed - /// from the expression, or null when the expression is invalid or the result is out of range. - private static BigInteger? ParsePointerOffsetExpression(string pointerOffsetExpression) + /// Full expression to parse. + /// Index to start parsing from. + /// Number of characters read from the expression. + /// The parsed pointer offset, or null if the expression is invalid. + private static PointerOffset? ParsePointerOffsetExpression(string expression, int startIndex, + out int readCount) { - // Use a regular expression to find all of the offset numbers to add up. - // The regex translates to: - // An optional + or - sign, followed by a hex number, optionally followed by any number of expressions comprised - // of a mandatory + or - sign followed by a hex number. - // The ^ and $ at the start and the end make sure that it won't match expressions that start or end with - // anything unexpected. - var offsetRegex = new Regex("^([+-]?[0-9a-f]{1,16})([+-][0-9a-f]{1,16})*$", - RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + readCount = 0; - // Replace all whitespaces in the expression to support a variety of cases. - // Examples: "1F 06 + 7C - 8" would parse the same as "1F06+7C-8", which seems like a good idea. - var match = offsetRegex.Match(pointerOffsetExpression.Replace(" ", string.Empty)); - - // If the regex doesn't match, the expression is invalid. - if (!match.Success) - return null; - - var offsets = match.Groups[2].Captures.Select(c => c.Value).Append(match.Groups[1].Value); + PointerOffset? sum = PointerOffset.Zero; + PointerOffset? currentNumber = PointerOffset.Zero; + bool lastCharWasOperator = false; + bool hasMeaningfulCharacters = false; - // Add up all the parts using BigIntegers. - // The regex has a 16-length limit for individual offsets, but we don't really care to check out of bounds - // values until everything is added up. For instance, we could have FFFFFFFFFFFFFFFF+1-1, which would be - // valid and result in a final value of FFFFFFFFFFFFFFFF. - BigInteger sum = 0; - foreach (var offset in offsets) + for (int i = startIndex; i < expression.Length; i++) { - // An empty offset means the whole pointer offset expression is invalid. - if (string.IsNullOrWhiteSpace(offset)) - return null; - - // Negative hex numbers cannot be parsed natively in .net, so we need to handle them manually - bool isNegative = offset[0] == '-'; - string parsableOffset = isNegative || offset[0] == '+' ? offset[1..] : offset; + readCount++; - if (!ulong.TryParse(parsableOffset, NumberStyles.HexNumber, CultureInfo.InvariantCulture, - out var offsetValue)) + char c = expression[i]; + + // Ignore all spaces everywhere in the expression + if (char.IsWhiteSpace(c)) + continue; + + if (c is '+' or '-') { - // The offset cannot be parsed, therefore the whole pointer offset is invalid. - return null; + if (lastCharWasOperator) + return null; // Two operators in a row is invalid + + // We just finished reading a number, or start reading a new one. + // We must add the current number to the sum, and reset the current number. + sum = sum.Value.Plus(currentNumber.Value); + if (sum == null) + return null; // Overflow or underflow + currentNumber = new PointerOffset(0, IsNegative: c == '-'); + + // Set the operator for the next number + lastCharWasOperator = true; + continue; } - sum = isNegative ? sum - offsetValue : sum + offsetValue; + if (c is ',') + break; // The sub-expression parsed by this method stops here + + // From here on, we are reading a non-operator character. + // This could be a number or an invalid character. + lastCharWasOperator = false; + byte value = CharToValue(c); + if (value == 255) + return null; // Invalid character + + hasMeaningfulCharacters = true; + currentNumber = currentNumber.Value.ShiftAndAdd(value); + if (currentNumber == null) + return null; // Overflow or underflow } - // With the final sum, check if the result is in the valid range for 64-bit addresses, positive or negative. - if (sum > ulong.MaxValue || sum < BigInteger.Negate(ulong.MaxValue)) + // We are done parsing the sub-expression. + // If it didn't contain any meaningful character, or ended with an operator, it is invalid. + if (lastCharWasOperator || !hasMeaningfulCharacters) return null; - return sum; + // Return the final sum. If it overflows or underflows, this will return null, as the expression is invalid. + return sum.Value.Plus(currentNumber.Value); + } + + /// + /// Converts a character to its hexadecimal value. + /// + /// Character to convert. + /// Hexadecimal value of the character, or 255 if the character is not a valid hexadecimal + /// character. + private static byte CharToValue(char c) + { + return c switch + { + >= '0' and <= '9' => (byte)(c - '0'), + >= 'A' and <= 'F' => (byte)(c - 'A' + 10), + >= 'a' and <= 'f' => (byte)(c - 'a' + 10), + _ => 255 + }; } /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() - { - return Expression; - } + public override string ToString() => Expression; } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs index a8a498a..b06f788 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs @@ -26,25 +26,25 @@ public Result EvaluateMemoryAddress(PointerPath } else { - var bigIntAddress = pointerPath.PointerOffsets.FirstOrDefault(); - baseAddress = bigIntAddress.ToUIntPtr(); + var firstOffset = pointerPath.PointerOffsets.FirstOrDefault(); + baseAddress = firstOffset.AsAddress(); if (baseAddress == null) - return new PathEvaluationFailureOnPointerOutOfRange(null, bigIntAddress); + return new PathEvaluationFailureOnPointerOutOfRange(null, firstOffset); } // Apply the base offset if there is one - if (pointerPath.BaseModuleOffset > 0) + if (pointerPath.BaseModuleOffset.Offset > 0) { - var bigIntAddress = baseAddress.Value.ToUInt64() + pointerPath.BaseModuleOffset; - baseAddress = bigIntAddress.ToUIntPtr(); + var baseAddressWithOffset = pointerPath.BaseModuleOffset.OffsetAddress(baseAddress.Value); + if (baseAddressWithOffset == null) + return new PathEvaluationFailureOnPointerOutOfRange(baseAddress, pointerPath.BaseModuleOffset); - if (baseAddress == null) - return new PathEvaluationFailureOnPointerOutOfRange(null, bigIntAddress); + baseAddress = baseAddressWithOffset.Value; } // Check if the base address is valid if (baseAddress == UIntPtr.Zero) - return new PathEvaluationFailureOnPointerOutOfRange(null, 0); + return new PathEvaluationFailureOnPointerOutOfRange(UIntPtr.Zero, PointerOffset.Zero); // Follow the pointer path offset by offset var currentAddress = baseAddress.Value; @@ -60,16 +60,13 @@ public Result EvaluateMemoryAddress(PointerPath // Apply the offset to the value we just read and check the result var offset = pointerPath.PointerOffsets[i]; - var nextValueBigInt = nextAddress.ToUInt64() + offset; - var nextValue = nextValueBigInt.ToUIntPtr(); + var nextValue = offset.OffsetAddress(nextAddress); // Check for invalid address values - if (nextValue == null) - return new PathEvaluationFailureOnPointerOutOfRange(currentAddress, nextValueBigInt); - if (nextValue == UIntPtr.Zero) - return new PathEvaluationFailureOnPointerOutOfRange(currentAddress, 0); + if (nextValue == null || nextValue.Value == UIntPtr.Zero) + return new PathEvaluationFailureOnPointerOutOfRange(nextAddress, offset); if (!IsBitnessCompatible(nextValue.Value)) - return new PathEvaluationFailureOnIncompatibleBitness(currentAddress); + return new PathEvaluationFailureOnIncompatibleBitness(nextAddress); // The next value has been vetted. Keep going with it as the current address currentAddress = nextValue.Value; diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs index 6d5c7b5..61f25b6 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs @@ -43,8 +43,16 @@ public Result Read(UIntPtr address) where T : struct return new ReadFailureOnIncompatibleBitness(address); // Get the size of the target type - int size = Marshal.SizeOf(); - + int size; + try + { + size = Marshal.SizeOf(); + } + catch (ArgumentException) + { + return new ReadFailureOnConversionFailure(); + } + // Read the bytes from the process memory var readResult = _osService.ReadProcessMemory(_processHandle, address, (ulong)size); if (readResult.IsFailure) diff --git a/src/MindControl/Results/PathEvaluationFailure.cs b/src/MindControl/Results/PathEvaluationFailure.cs index c83ec0b..ad3892c 100644 --- a/src/MindControl/Results/PathEvaluationFailure.cs +++ b/src/MindControl/Results/PathEvaluationFailure.cs @@ -1,6 +1,4 @@ -using System.Numerics; - -namespace MindControl.Results; +namespace MindControl.Results; /// /// Represents a reason for a path evaluation operation to fail. @@ -67,17 +65,16 @@ public override string ToString() /// Represents a failure in a path evaluation operation when a pointer in the path is out of the target process /// address space. /// -/// Address where the value causing the issue was read. May be null if the first address +/// Address that triggered the failure after the offset. May be null if the first address /// in the path caused the failure. -/// Address that caused the failure. The address is a BigInteger because it may be beyond the -/// range of a UIntPtr. -public record PathEvaluationFailureOnPointerOutOfRange(UIntPtr? PreviousAddress, BigInteger Address) +/// Offset that caused the failure. +public record PathEvaluationFailureOnPointerOutOfRange(UIntPtr? PreviousAddress, PointerOffset Offset) : PathEvaluationFailure(PathEvaluationFailureReason.PointerOutOfRange) { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"The pointer path evaluated a pointer to an address that is out of the target process address space range: {Address}."; + => "The pointer path evaluated a pointer to an address that is out of the target process address space range."; } /// diff --git a/test/MindControl.Benchmark/BenchmarkMemorySetup.cs b/test/MindControl.Benchmark/BenchmarkMemorySetup.cs new file mode 100644 index 0000000..b95ce9c --- /dev/null +++ b/test/MindControl.Benchmark/BenchmarkMemorySetup.cs @@ -0,0 +1,81 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Threading; +using Memory; + +namespace MindControl.Benchmark; + +public class BenchmarkMemorySetup : IDisposable +{ + /// + /// Gets the process that runs the target project. + /// + public Process TargetProcess { get; } + + /// + /// Gets the address of the outer class instance in the target project. + /// + public UIntPtr OuterClassPointer { get; } + + /// + /// Gets the MindControl process memory instance attached to the target project. + /// + public ProcessMemory MindControlProcessMemory { get; } + + /// + /// Gets the Memory.dll process memory instance attached to the target project. + /// + public Mem MemoryDllMem { get; } + + private BenchmarkMemorySetup(Process targetProcess, UIntPtr outerClassPointer, + ProcessMemory mindControlProcessMemory, Mem memoryDllMem) + { + TargetProcess = targetProcess; + OuterClassPointer = outerClassPointer; + MindControlProcessMemory = mindControlProcessMemory; + MemoryDllMem = memoryDllMem; + } + + /// + /// Sets up the benchmark memory setup. + /// + /// The benchmark memory setup. + public static BenchmarkMemorySetup Setup() + { + var targetProcess = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "./MindControl.Test.TargetApp.exe", + RedirectStandardOutput = true, + RedirectStandardInput = true + } + }; + targetProcess.Start(); + + string line = targetProcess.StandardOutput.ReadLine(); + if (!UIntPtr.TryParse(line, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var outerClassPointer)) + throw new Exception($"Could not read the outer class pointer output by the app: \"{line}\"."); + + var mindControlProcessMemory = ProcessMemory.OpenProcessById(targetProcess.Id); + var memoryDllMem = new Mem(); + memoryDllMem.OpenProcess(targetProcess.Id); + + return new BenchmarkMemorySetup(targetProcess, outerClassPointer, mindControlProcessMemory, memoryDllMem); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + MemoryDllMem.CloseProcess(); + TargetProcess.Kill(); + TargetProcess.Dispose(); + MindControlProcessMemory.Dispose(); + + // Make sure the process is exited before going on + Thread.Sleep(250); + } +} \ No newline at end of file diff --git a/test/MindControl.Benchmark/Benchmarks.cs b/test/MindControl.Benchmark/Benchmarks.cs deleted file mode 100644 index f576475..0000000 --- a/test/MindControl.Benchmark/Benchmarks.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Diagnostics; -using System.Globalization; -using System.Text; -using System.Threading; -using BenchmarkDotNet.Attributes; -using Memory; - -namespace MindControl.Benchmark; - -[MemoryDiagnoser] -public class Benchmarks -{ - private Process _targetProcess; - private ProcessMemory _processMemory; - private UIntPtr _outerClassPointer; - private Mem _mem; - - [GlobalSetup] - public void GlobalSetup() - { - _targetProcess = new Process - { - StartInfo = new ProcessStartInfo - { - FileName = "./MindControl.Test.TargetApp.exe", - RedirectStandardOutput = true, - RedirectStandardInput = true, - StandardOutputEncoding = Encoding.UTF8 - } - }; - _targetProcess.Start(); - - string line = _targetProcess.StandardOutput.ReadLine(); - if (!UIntPtr.TryParse(line, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var outerClassPointer)) - throw new Exception($"Could not read the outer class pointer output by the app: \"{line}\"."); - - _outerClassPointer = outerClassPointer; - _processMemory = ProcessMemory.OpenProcessById(_targetProcess.Id); - _mem = new Mem(); - _mem.OpenProcess(_targetProcess.Id); - } - - [GlobalCleanup] - public void GlobalCleanup() - { - _processMemory.Dispose(); - _targetProcess.Kill(); - - // Make sure the process is exited before going on - Thread.Sleep(250); - } - - [Benchmark(Description = "MindControl Read")] - public int MindControlReadGenericType() - { - var result = _processMemory.Read(_outerClassPointer + 0x38).Value; - if (result != -7651) - throw new Exception("Unexpected result"); - return result; - } - - [Benchmark(Description = "MindControl Read(Type)")] - public int MindControlReadObject() - { - var result = (int)_processMemory.Read(typeof(int), _outerClassPointer + 0x38).Value; - if (result != -7651) - throw new Exception("Unexpected result"); - return result; - } - - [Benchmark(Description = "Memory.dll ReadInt")] - public int MemoryReadInt() - { - var address = _outerClassPointer + 0x38; - var result = _mem.ReadInt(address.ToString("X")); - if (result != -7651) - throw new Exception("Unexpected result"); - return result; - } - - [Benchmark(Description = "Memory.dll ReadMemory")] - public int MemoryReadGeneric() - { - var address = _outerClassPointer + 0x38; - var result = _mem.ReadMemory(address.ToString("X")); - if (result != -7651) - throw new Exception("Unexpected result"); - return result; - } -} \ No newline at end of file diff --git a/test/MindControl.Benchmark/PointerPathBenchmark.cs b/test/MindControl.Benchmark/PointerPathBenchmark.cs new file mode 100644 index 0000000..5b2b25a --- /dev/null +++ b/test/MindControl.Benchmark/PointerPathBenchmark.cs @@ -0,0 +1,16 @@ +using BenchmarkDotNet.Attributes; + +namespace MindControl.Benchmark; + +[MemoryDiagnoser] +public class PointerPathBenchmark +{ + [Benchmark(Description = "Minimal path")] + public PointerPath MinimalPath() => new("0"); + + [Benchmark(Description = "Base address with offsets")] + public PointerPath BaseAddressWithOffsets() => "1F71CD5CD88+10,10,0"; + + [Benchmark(Description = "Module and offsets")] + public PointerPath Default() => "MyDefaultModule.dll+10,10,0"; +} \ No newline at end of file diff --git a/test/MindControl.Benchmark/Program.cs b/test/MindControl.Benchmark/Program.cs index f1c6a95..c8d403d 100644 --- a/test/MindControl.Benchmark/Program.cs +++ b/test/MindControl.Benchmark/Program.cs @@ -1,9 +1,9 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; -using MindControl.Benchmark; var config = DefaultConfig.Instance // We have to disable the optimizations validator because Memory.dll is not optimized // But we should always run benchmarks in Release in any case. .WithOptions(ConfigOptions.DisableOptimizationsValidator); -BenchmarkRunner.Run(config, args); + +var summaries = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); diff --git a/test/MindControl.Benchmark/ReadIntByAddressBenchmark.cs b/test/MindControl.Benchmark/ReadIntByAddressBenchmark.cs new file mode 100644 index 0000000..43d33e1 --- /dev/null +++ b/test/MindControl.Benchmark/ReadIntByAddressBenchmark.cs @@ -0,0 +1,55 @@ +using System; +using BenchmarkDotNet.Attributes; + +namespace MindControl.Benchmark; + +[MemoryDiagnoser] +[MarkdownExporter] +public class ReadIntByAddressBenchmark +{ + private BenchmarkMemorySetup _setup; + + [GlobalSetup] + public void GlobalSetup() => _setup = BenchmarkMemorySetup.Setup(); + + [GlobalCleanup] + public void GlobalCleanup() => _setup.Dispose(); + + [Benchmark(Description = "MindControl Read")] + public int MindControlReadGenericType() + { + var result = _setup.MindControlProcessMemory.Read(_setup.OuterClassPointer + 0x38).Value; + if (result != -7651) + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "MindControl Read(Type)")] + public int MindControlReadObject() + { + var result = (int)_setup.MindControlProcessMemory.Read(typeof(int), _setup.OuterClassPointer + 0x38).Value; + if (result != -7651) + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "Memory.dll ReadInt", Baseline = true)] + public int MemoryReadInt() + { + UIntPtr address = _setup.OuterClassPointer + 0x38; + int result = _setup.MemoryDllMem.ReadInt(address.ToString("X")); + if (result != -7651) + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "Memory.dll ReadMemory")] + public int MemoryReadGeneric() + { + UIntPtr address = _setup.OuterClassPointer + 0x38; + var result = _setup.MemoryDllMem.ReadMemory(address.ToString("X")); + if (result != -7651) + throw new Exception("Unexpected result"); + return result; + } +} \ No newline at end of file diff --git a/test/MindControl.Benchmark/ReadLongByPointerPathBenchmark.cs b/test/MindControl.Benchmark/ReadLongByPointerPathBenchmark.cs new file mode 100644 index 0000000..2463fe2 --- /dev/null +++ b/test/MindControl.Benchmark/ReadLongByPointerPathBenchmark.cs @@ -0,0 +1,78 @@ +using System; +using BenchmarkDotNet.Attributes; + +namespace MindControl.Benchmark; + +[MemoryDiagnoser] +[MarkdownExporter] +public class ReadLongByPointerPathBenchmark +{ + private BenchmarkMemorySetup _setup; + private string _pathString; + private PointerPath _pointerPath; + + [GlobalSetup] + public void GlobalSetup() + { + _setup = BenchmarkMemorySetup.Setup(); + _pathString = $"{_setup.OuterClassPointer:X}+10,10"; + _pointerPath = _pathString; + } + + [GlobalCleanup] + public void GlobalCleanup() => _setup.Dispose(); + + [Benchmark(Description = "MindControl Read (reused path)")] + public long MindControlReadGenericTypeReuse() + { + long result = _setup.MindControlProcessMemory.Read(_pointerPath).Value; + if (result != -1) + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "MindControl Read (dynamic path)")] + public long MindControlReadGenericTypeDynamic() + { + long result = _setup.MindControlProcessMemory.Read(_pathString).Value; + if (result != -1) + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "MindControl Read(Type) (reused path)")] + public long MindControlReadObjectReuse() + { + var result = (long)_setup.MindControlProcessMemory.Read(typeof(long), _pointerPath).Value; + if (result != -1) + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "MindControl Read(Type) (dynamic path)")] + public long MindControlReadObjectDynamic() + { + var result = (long)_setup.MindControlProcessMemory.Read(typeof(long), _pathString).Value; + if (result != -1) + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "Memory.dll ReadLong", Baseline = true)] + public long MemoryReadLong() + { + long result = _setup.MemoryDllMem.ReadLong(_pathString); + if (result != -1) + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "Memory.dll ReadMemory")] + public long MemoryReadGeneric() + { + var result = _setup.MemoryDllMem.ReadMemory(_pathString); + if (result != -1) + throw new Exception("Unexpected result"); + return result; + } +} \ No newline at end of file diff --git a/test/MindControl.Test/AddressingTests/PointerPathTest.cs b/test/MindControl.Test/AddressingTests/PointerPathTest.cs index 70857d1..383fd9f 100644 --- a/test/MindControl.Test/AddressingTests/PointerPathTest.cs +++ b/test/MindControl.Test/AddressingTests/PointerPathTest.cs @@ -13,8 +13,8 @@ public readonly struct ExpressionTestCase public string Expression { get; init; } public bool ShouldBeValid { get; init; } public string? ExpectedModuleName { get; init; } - public BigInteger ExpectedModuleOffset { get; init; } - public BigInteger[]? ExpectedPointerOffsets { get; init; } + public PointerOffset ExpectedModuleOffset { get; init; } + public PointerOffset[]? ExpectedPointerOffsets { get; init; } public bool Expect64BitOnly { get; init; } public string Explanation { get; init; } public override string ToString() => Expression; @@ -31,8 +31,8 @@ public readonly struct ExpressionTestCase Expression = "mymoduleName.exe+1F016644,13,A0,0", ShouldBeValid = true, ExpectedModuleName = "mymoduleName.exe", - ExpectedModuleOffset = 0x1F016644, - ExpectedPointerOffsets = new BigInteger[] { 0x13, 0xA0, 0 }, + ExpectedModuleOffset = new(0x1F016644, false), + ExpectedPointerOffsets = new[] { new(0x13, false), new(0xA0, false), PointerOffset.Zero }, Explanation = "Expressions must support a base module name, static offsets (+/-) and pointer offsets (,)." }, new ExpressionTestCase @@ -40,8 +40,8 @@ public readonly struct ExpressionTestCase Expression = " mymoduleName.exe + 1F016644 , 13 ,A0 ,0 ", ShouldBeValid = true, ExpectedModuleName = "mymoduleName.exe", - ExpectedModuleOffset = 0x1F016644, - ExpectedPointerOffsets = new BigInteger[] { 0x13, 0xA0, 0 }, + ExpectedModuleOffset = new(0x1F016644, false), + ExpectedPointerOffsets = new[] { new(0x13, false), new(0xA0, false), PointerOffset.Zero }, Explanation = "Arbitrary whitespaces before and after any subexpression must be supported." }, new ExpressionTestCase @@ -49,8 +49,8 @@ public readonly struct ExpressionTestCase Expression = "\"mymoduleName.exe\"+1F01664D", ShouldBeValid = true, ExpectedModuleName = "mymoduleName.exe", - ExpectedModuleOffset = 0x1F01664D, - ExpectedPointerOffsets = Array.Empty(), + ExpectedModuleOffset = new(0x1F01664D, false), + ExpectedPointerOffsets = Array.Empty(), Explanation = "Module names with double-quotes must be supported, and interpreted without double-quotes." }, new ExpressionTestCase @@ -58,8 +58,8 @@ public readonly struct ExpressionTestCase Expression = "mymoduleName.anything+1F01664D", ShouldBeValid = true, ExpectedModuleName = "mymoduleName.anything", - ExpectedModuleOffset = 0x1F01664D, - ExpectedPointerOffsets = Array.Empty(), + ExpectedModuleOffset = new(0x1F01664D, false), + ExpectedPointerOffsets = Array.Empty(), Explanation = "Module names with any extension must be supported." }, new ExpressionTestCase @@ -67,8 +67,8 @@ public readonly struct ExpressionTestCase Expression = "mymoduleName+1F01664D", ShouldBeValid = true, ExpectedModuleName = "mymoduleName", - ExpectedModuleOffset = 0x1F01664D, - ExpectedPointerOffsets = Array.Empty(), + ExpectedModuleOffset = new(0x1F01664D, false), + ExpectedPointerOffsets = Array.Empty(), Explanation = "Module names without an extension must be supported." }, new ExpressionTestCase @@ -76,8 +76,8 @@ public readonly struct ExpressionTestCase Expression = "mymoduleName.exe,0F", ShouldBeValid = true, ExpectedModuleName = "mymoduleName.exe", - ExpectedModuleOffset = 0, - ExpectedPointerOffsets = new BigInteger[] { 0x0F }, + ExpectedModuleOffset = PointerOffset.Zero, + ExpectedPointerOffsets = new PointerOffset[] { new(0x0F, false) }, Explanation = "Module names with no static offsets must be supported." }, new ExpressionTestCase @@ -85,8 +85,8 @@ public readonly struct ExpressionTestCase Expression = "mymoduleName.exe-0F", ShouldBeValid = true, ExpectedModuleName = "mymoduleName.exe", - ExpectedModuleOffset = -0x0F, - ExpectedPointerOffsets = Array.Empty(), + ExpectedModuleOffset = new(0x0F, true), + ExpectedPointerOffsets = Array.Empty(), Explanation = "Module names with a negative static offset must be supported." }, new ExpressionTestCase @@ -94,8 +94,8 @@ public readonly struct ExpressionTestCase Expression = "mymoduleName.exe", ShouldBeValid = true, ExpectedModuleName = "mymoduleName.exe", - ExpectedModuleOffset = 0, - ExpectedPointerOffsets = Array.Empty(), + ExpectedModuleOffset = PointerOffset.Zero, + ExpectedPointerOffsets = Array.Empty(), Explanation = "Module names on their own must be supported." }, new ExpressionTestCase @@ -103,8 +103,8 @@ public readonly struct ExpressionTestCase Expression = "my1FmoduleName.exe+1F", ShouldBeValid = true, ExpectedModuleName = "my1FmoduleName.exe", - ExpectedModuleOffset = 0x1F, - ExpectedPointerOffsets = Array.Empty(), + ExpectedModuleOffset = new(0x1F, false), + ExpectedPointerOffsets = Array.Empty(), Explanation = "Module names containing numerals must be supported." }, new ExpressionTestCase @@ -112,8 +112,8 @@ public readonly struct ExpressionTestCase Expression = "1FmymoduleName.exe+1F", ShouldBeValid = true, ExpectedModuleName = "1FmymoduleName.exe", - ExpectedModuleOffset = 0x1F, - ExpectedPointerOffsets = Array.Empty(), + ExpectedModuleOffset = new(0x1F, false), + ExpectedPointerOffsets = Array.Empty(), Explanation = "Module names starting with numerals must be supported." }, new ExpressionTestCase @@ -121,8 +121,9 @@ public readonly struct ExpressionTestCase Expression = "1F016644,13,A0,0", ShouldBeValid = true, ExpectedModuleName = null, - ExpectedModuleOffset = 0, - ExpectedPointerOffsets = new BigInteger[] { 0x1F016644, 0x13, 0xA0, 0x0 }, + ExpectedModuleOffset = PointerOffset.Zero, + ExpectedPointerOffsets = new[] { new(0x1F016644, false), new(0x13, false), + new(0xA0, false), PointerOffset.Zero }, Explanation = "Expressions without a module name must be supported." }, new ExpressionTestCase @@ -130,8 +131,8 @@ public readonly struct ExpressionTestCase Expression = "AF016644", ShouldBeValid = true, ExpectedModuleName = null, - ExpectedModuleOffset = 0, - ExpectedPointerOffsets = new BigInteger[] { 0xAF016644 }, + ExpectedModuleOffset = PointerOffset.Zero, + ExpectedPointerOffsets = new PointerOffset[] { new(0xAF016644, false) }, Explanation = "A static address by itself must be supported." }, new ExpressionTestCase @@ -139,8 +140,8 @@ public readonly struct ExpressionTestCase Expression = "mymoduleName.exe+4,-2F", ShouldBeValid = true, ExpectedModuleName = "mymoduleName.exe", - ExpectedModuleOffset = 4, - ExpectedPointerOffsets = new BigInteger[] { -0x2F }, + ExpectedModuleOffset = new(0x4, false), + ExpectedPointerOffsets = new PointerOffset[] { new(0x2F, true) }, Explanation = "Negative pointer offsets must be supported." }, new ExpressionTestCase @@ -148,8 +149,8 @@ public readonly struct ExpressionTestCase Expression = "mymoduleName.exe+6A-2C+8", ShouldBeValid = true, ExpectedModuleName = "mymoduleName.exe", - ExpectedModuleOffset = 0x46, - ExpectedPointerOffsets = Array.Empty(), + ExpectedModuleOffset = new(0x46, false), + ExpectedPointerOffsets = Array.Empty(), Explanation = "Several static offsets added together must be supported." }, new ExpressionTestCase @@ -157,8 +158,8 @@ public readonly struct ExpressionTestCase Expression = "mymoduleName.exe,6A-2C+8", ShouldBeValid = true, ExpectedModuleName = "mymoduleName.exe", - ExpectedModuleOffset = 0, - ExpectedPointerOffsets = new BigInteger[] { 0x46 }, + ExpectedModuleOffset = PointerOffset.Zero, + ExpectedPointerOffsets = new PointerOffset[] { new(0x46, false) }, Explanation = "Static offsets added together within a pointer offset must be supported." }, new ExpressionTestCase @@ -166,8 +167,8 @@ public readonly struct ExpressionTestCase Expression = "mymoduleName.exe+FFFFFFFF,FFFFFFFF", ShouldBeValid = true, ExpectedModuleName = "mymoduleName.exe", - ExpectedModuleOffset = 0xFFFFFFFF, - ExpectedPointerOffsets = new BigInteger[] { 0xFFFFFFFF }, + ExpectedModuleOffset = new(0xFFFFFFFF, false), + ExpectedPointerOffsets = new PointerOffset[] { new(0xFFFFFFFF, false) }, Expect64BitOnly = false, Explanation = "Offsets within the 32-bit addressing boundaries must be supported in all cases." }, @@ -176,24 +177,19 @@ public readonly struct ExpressionTestCase Expression = "mymoduleName.exe+FFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFF", ShouldBeValid = true, ExpectedModuleName = "mymoduleName.exe", - ExpectedModuleOffset = 0xFFFFFFFFFFFFFFFF, - ExpectedPointerOffsets = new BigInteger[] { 0xFFFFFFFFFFFFFFFF }, + ExpectedModuleOffset = new(0xFFFFFFFFFFFFFFFF, false), + ExpectedPointerOffsets = new PointerOffset[] { new(0xFFFFFFFFFFFFFFFF, false) }, Expect64BitOnly = true, Explanation = "Offsets within the 64-bit addressing boundaries must be supported in 64-bit only mode." }, + + // Non valid expression cases new ExpressionTestCase { Expression = "mymoduleName.exe+FFFFFFFFFFFFFFFF+1-1,FFFFFFFFFFFFFFFF+1-1", - ShouldBeValid = true, - ExpectedModuleName = "mymoduleName.exe", - ExpectedModuleOffset = 0xFFFFFFFFFFFFFFFF, - ExpectedPointerOffsets = new BigInteger[] { 0xFFFFFFFFFFFFFFFF }, - Expect64BitOnly = true, - Explanation = - "Offset boundaries must apply only after all computations are done. Only the final value must respect the boundaries." + ShouldBeValid = false, + Explanation = "Offsets sub-summing up to over the 64-bit addressing boundaries must be invalid, even if the sum as a whole is within the boundaries." }, - - // Non valid expression cases new ExpressionTestCase { Expression = "mymoduleName.exe++1F016644", diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs index fbb65ad..dea18c2 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs @@ -1,5 +1,4 @@ -using System.Numerics; -using MindControl.Results; +using MindControl.Results; using NUnit.Framework; namespace MindControl.Test.ProcessMemoryTests; @@ -42,8 +41,8 @@ public void EvaluateOverMaxPointerValueTest() var error = result.Error; Assert.That(error, Is.TypeOf()); var pathError = (PathEvaluationFailureOnPointerOutOfRange)error; - Assert.That(pathError.Address, Is.EqualTo(new BigInteger(ulong.MaxValue) + 1)); - Assert.That(pathError.PreviousAddress, Is.EqualTo(expectedPreviousAddress)); + Assert.That(pathError.Offset, Is.EqualTo(new PointerOffset(1, false))); + Assert.That(pathError.PreviousAddress, Is.EqualTo(UIntPtr.MaxValue)); } /// @@ -82,8 +81,8 @@ public void EvaluateOnZeroPointerTest() var error = result.Error; Assert.That(error, Is.TypeOf()); var pathError = (PathEvaluationFailureOnPointerOutOfRange)error; - Assert.That(pathError.Address, Is.EqualTo(new BigInteger(0))); - Assert.That(pathError.PreviousAddress, Is.Null); + Assert.That(pathError.Offset, Is.EqualTo(PointerOffset.Zero)); + Assert.That(pathError.PreviousAddress, Is.EqualTo(UIntPtr.Zero)); } /// diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index 2d0eaca..a05a703 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -1,5 +1,4 @@ -using System.Numerics; -using System.Text; +using System.Text; using MindControl.Results; using NUnit.Framework; @@ -292,9 +291,8 @@ public void ReadOverMaxPointerValueTest() var pathError = ((ReadFailureOnPointerPathEvaluation)error).PathEvaluationFailure; Assert.That(pathError, Is.TypeOf(typeof(PathEvaluationFailureOnPointerOutOfRange))); var outOfRangeError = (PathEvaluationFailureOnPointerOutOfRange)pathError; - Assert.That(outOfRangeError.Address, Is.EqualTo(new BigInteger(ulong.MaxValue) + 1)); - Assert.That(outOfRangeError.PreviousAddress, Is.Not.Null); - Assert.That(outOfRangeError.PreviousAddress, Is.Not.EqualTo(UIntPtr.Zero)); + Assert.That(outOfRangeError.Offset, Is.EqualTo(new PointerOffset(1, false))); + Assert.That(outOfRangeError.PreviousAddress, Is.EqualTo(UIntPtr.MaxValue)); } /// From 3563dd134290e3a987c52b2fcc2e82a853dd95b5 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 25 May 2024 09:41:45 +0200 Subject: [PATCH 11/66] Removed type-specific public write methods --- .../ProcessMemory/ProcessMemory.Write.cs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs index 56e84da..8281943 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs @@ -68,7 +68,7 @@ public Result Write(UIntPtr address, T value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteBool(PointerPath path, bool value, + private Result WriteBool(PointerPath path, bool value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { var addressResult = EvaluateMemoryAddress(path); @@ -85,7 +85,7 @@ public Result WriteBool(PointerPath path, bool value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteBool(UIntPtr address, bool value, + private Result WriteBool(UIntPtr address, bool value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, new[] { (byte)(value ? 1 : 0) }, memoryProtectionStrategy); @@ -97,7 +97,7 @@ public Result WriteBool(UIntPtr address, bool value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteByte(PointerPath path, byte value, + private Result WriteByte(PointerPath path, byte value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { var addressResult = EvaluateMemoryAddress(path); @@ -113,7 +113,7 @@ public Result WriteByte(PointerPath path, byte value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteByte(UIntPtr address, byte value, + private Result WriteByte(UIntPtr address, byte value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, new[] { value }, memoryProtectionStrategy); @@ -125,7 +125,7 @@ public Result WriteByte(UIntPtr address, byte value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteShort(PointerPath path, short value, + private Result WriteShort(PointerPath path, short value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { var addressResult = EvaluateMemoryAddress(path); @@ -141,7 +141,7 @@ public Result WriteShort(PointerPath path, short value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteShort(UIntPtr address, short value, + private Result WriteShort(UIntPtr address, short value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); @@ -153,7 +153,7 @@ public Result WriteShort(UIntPtr address, short value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteUShort(PointerPath path, ushort value, + private Result WriteUShort(PointerPath path, ushort value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { var addressResult = EvaluateMemoryAddress(path); @@ -169,7 +169,7 @@ public Result WriteUShort(PointerPath path, ushort value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteUShort(UIntPtr address, ushort value, + private Result WriteUShort(UIntPtr address, ushort value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); @@ -181,7 +181,7 @@ public Result WriteUShort(UIntPtr address, ushort value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteInt(PointerPath path, int value, + private Result WriteInt(PointerPath path, int value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { var addressResult = EvaluateMemoryAddress(path); @@ -197,7 +197,7 @@ public Result WriteInt(PointerPath path, int value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteInt(UIntPtr address, int value, + private Result WriteInt(UIntPtr address, int value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); @@ -209,7 +209,7 @@ public Result WriteInt(UIntPtr address, int value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteUInt(PointerPath path, uint value, + private Result WriteUInt(PointerPath path, uint value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { var addressResult = EvaluateMemoryAddress(path); @@ -225,7 +225,7 @@ public Result WriteUInt(PointerPath path, uint value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteUInt(UIntPtr address, uint value, + private Result WriteUInt(UIntPtr address, uint value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); @@ -237,7 +237,7 @@ public Result WriteUInt(UIntPtr address, uint value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteIntPtr(PointerPath path, IntPtr value, + private Result WriteIntPtr(PointerPath path, IntPtr value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { var addressResult = EvaluateMemoryAddress(path); @@ -253,7 +253,7 @@ public Result WriteIntPtr(PointerPath path, IntPtr value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteIntPtr(UIntPtr address, IntPtr value, + private Result WriteIntPtr(UIntPtr address, IntPtr value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, value.ToBytes(_is64Bits), memoryProtectionStrategy); @@ -265,7 +265,7 @@ public Result WriteIntPtr(UIntPtr address, IntPtr value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteFloat(PointerPath path, float value, + private Result WriteFloat(PointerPath path, float value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { var addressResult = EvaluateMemoryAddress(path); @@ -281,7 +281,7 @@ public Result WriteFloat(PointerPath path, float value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteFloat(UIntPtr address, float value, + private Result WriteFloat(UIntPtr address, float value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); @@ -293,7 +293,7 @@ public Result WriteFloat(UIntPtr address, float value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteLong(PointerPath path, long value, + private Result WriteLong(PointerPath path, long value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { var addressResult = EvaluateMemoryAddress(path); @@ -309,7 +309,7 @@ public Result WriteLong(PointerPath path, long value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteLong(UIntPtr address, long value, + private Result WriteLong(UIntPtr address, long value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); @@ -321,7 +321,7 @@ public Result WriteLong(UIntPtr address, long value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteULong(PointerPath path, ulong value, + private Result WriteULong(PointerPath path, ulong value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { var addressResult = EvaluateMemoryAddress(path); @@ -337,7 +337,7 @@ public Result WriteULong(PointerPath path, ulong value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteULong(UIntPtr address, ulong value, + private Result WriteULong(UIntPtr address, ulong value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); @@ -349,7 +349,7 @@ public Result WriteULong(UIntPtr address, ulong value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteDouble(PointerPath path, double value, + private Result WriteDouble(PointerPath path, double value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { var addressResult = EvaluateMemoryAddress(path); @@ -365,7 +365,7 @@ public Result WriteDouble(PointerPath path, double value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteDouble(UIntPtr address, double value, + private Result WriteDouble(UIntPtr address, double value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); From 686274a3b1eb5d78deb6f0ebafe33fd7e52480ad Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 25 May 2024 09:49:13 +0200 Subject: [PATCH 12/66] Added benchmarks for write operations --- .../WriteIntByAddressBenchmark.cs | 35 +++++++++++++ .../WriteLongByPointerPathBenchmark.cs | 50 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 test/MindControl.Benchmark/WriteIntByAddressBenchmark.cs create mode 100644 test/MindControl.Benchmark/WriteLongByPointerPathBenchmark.cs diff --git a/test/MindControl.Benchmark/WriteIntByAddressBenchmark.cs b/test/MindControl.Benchmark/WriteIntByAddressBenchmark.cs new file mode 100644 index 0000000..15867d3 --- /dev/null +++ b/test/MindControl.Benchmark/WriteIntByAddressBenchmark.cs @@ -0,0 +1,35 @@ +using System; +using BenchmarkDotNet.Attributes; + +namespace MindControl.Benchmark; + +[MemoryDiagnoser] +[MarkdownExporter] +public class WriteIntByAddressBenchmark +{ + private BenchmarkMemorySetup _setup; + + [GlobalSetup] + public void GlobalSetup() => _setup = BenchmarkMemorySetup.Setup(); + + [GlobalCleanup] + public void GlobalCleanup() => _setup.Dispose(); + + [Benchmark(Description = "MindControl Write")] + public void MindControlReadGenericType() + { + var result = _setup.MindControlProcessMemory.Write(_setup.OuterClassPointer + 0x38, -7651, + MemoryProtectionStrategy.Ignore); + if (!result.IsSuccess) + throw new Exception("Write failed"); + } + + [Benchmark(Description = "Memory.dll WriteMemory", Baseline = true)] + public void MemoryReadInt() + { + UIntPtr address = _setup.OuterClassPointer + 0x38; + bool result = _setup.MemoryDllMem.WriteMemory(address.ToString("X"), "int", "-7651"); + if (!result) + throw new Exception("Write failed"); + } +} \ No newline at end of file diff --git a/test/MindControl.Benchmark/WriteLongByPointerPathBenchmark.cs b/test/MindControl.Benchmark/WriteLongByPointerPathBenchmark.cs new file mode 100644 index 0000000..b797184 --- /dev/null +++ b/test/MindControl.Benchmark/WriteLongByPointerPathBenchmark.cs @@ -0,0 +1,50 @@ +using System; +using BenchmarkDotNet.Attributes; + +namespace MindControl.Benchmark; + +[MemoryDiagnoser] +[MarkdownExporter] +public class WriteLongByPointerPathBenchmark +{ + private BenchmarkMemorySetup _setup; + private string _pathString; + private PointerPath _pointerPath; + + [GlobalSetup] + public void GlobalSetup() + { + _setup = BenchmarkMemorySetup.Setup(); + _pathString = $"{_setup.OuterClassPointer:X}+10,10"; + _pointerPath = _pathString; + } + + [GlobalCleanup] + public void GlobalCleanup() => _setup.Dispose(); + + [Benchmark(Description = "MindControl Write (reused path)")] + public void MindControlReadGenericTypeReuse() + { + var result = _setup.MindControlProcessMemory.Write(_pointerPath, -496873331231411L, + MemoryProtectionStrategy.Ignore); + if (!result.IsSuccess) + throw new Exception("Write failed"); + } + + [Benchmark(Description = "MindControl Write (dynamic path)")] + public void MindControlReadGenericTypeDynamic() + { + var result = _setup.MindControlProcessMemory.Write(_pathString, -496873331231411L, + MemoryProtectionStrategy.Ignore); + if (!result.IsSuccess) + throw new Exception("Write failed"); + } + + [Benchmark(Description = "Memory.dll WriteMemory", Baseline = true)] + public void MemoryReadGeneric() + { + var result = _setup.MemoryDllMem.WriteMemory(_pathString, "long", "-496873331231411"); + if (!result) + throw new Exception("Write failed"); + } +} \ No newline at end of file From 1568d3553a3ec7ed904b07f3cbcdbbe552e9581d Mon Sep 17 00:00:00 2001 From: Doublevil Date: Fri, 31 May 2024 06:53:43 +0200 Subject: [PATCH 13/66] String rework with FindStringSettings and ReadRawString --- .../Allocation/MemoryReservation.cs | 5 + .../Internal/ConversionExtensions.cs | 35 +- .../ProcessMemory/ProcessMemory.Read.cs | 428 ++++++++++------ .../Results/FindStringSettingsFailure.cs | 102 ++++ .../Results/PathEvaluationFailure.cs | 6 +- src/MindControl/Results/ReadFailure.cs | 12 +- src/MindControl/Results/WriteFailure.cs | 24 +- src/MindControl/StringSettings.cs | 300 ++++++++--- .../ProcessMemoryEvaluateTest.cs | 11 +- .../ProcessMemoryReadTest.cs | 484 +++++++++++++----- 10 files changed, 1022 insertions(+), 385 deletions(-) create mode 100644 src/MindControl/Results/FindStringSettingsFailure.cs diff --git a/src/MindControl/Allocation/MemoryReservation.cs b/src/MindControl/Allocation/MemoryReservation.cs index c595483..f94a6d5 100644 --- a/src/MindControl/Allocation/MemoryReservation.cs +++ b/src/MindControl/Allocation/MemoryReservation.cs @@ -12,6 +12,11 @@ public class MemoryReservation /// Gets the memory range of this reservation. /// public MemoryRange Range { get; } + + /// + /// Gets the starting address of the reservation. + /// + public UIntPtr Address => Range.Start; /// /// Gets the allocation that handles this reservation. diff --git a/src/MindControl/Internal/ConversionExtensions.cs b/src/MindControl/Internal/ConversionExtensions.cs index 338c259..f966283 100644 --- a/src/MindControl/Internal/ConversionExtensions.cs +++ b/src/MindControl/Internal/ConversionExtensions.cs @@ -1,6 +1,6 @@ using System.Numerics; using System.Runtime.InteropServices; -using System.Runtime.Serialization.Formatters.Binary; +using System.Text; namespace MindControl.Internal; @@ -17,10 +17,20 @@ internal static class ConversionExtensions /// Bytes to read. /// /// Thrown when the array's length is more than 8. - public static ulong ReadUnsignedNumber(this byte[] bytes) + public static ulong ReadUnsignedNumber(this byte[] bytes) => ReadUnsignedNumber(bytes.AsSpan()); + + /// + /// Reads an unsigned number from the byte span. + /// The byte span can be of any length comprised between 0 and 8 included. + /// The number returned will be an unsigned long in all cases. + /// + /// Bytes to read. + /// + /// Thrown when the span is too large. + public static ulong ReadUnsignedNumber(this Span bytes) { if (bytes.Length > 8) - throw new ArgumentException("The byte array cannot be read as a ulong because it is longer than 8 bytes.", + throw new ArgumentException("The bytes cannot be read as a ulong because there are more than 8 bytes.", nameof(bytes)); ulong result = 0; @@ -109,4 +119,23 @@ public static byte[] ToBytes(this object? value) return StructToBytes(value); } + + /// Caches the null terminator byte sequences for each encoding. Used in . + /// + private static readonly Dictionary StringNullTerminatorsCache = new(); + + /// + /// Gets the null terminator byte sequence for this encoding. + /// + /// Target encoding. + /// The null terminator byte sequence. + public static byte[] GetNullTerminator(this Encoding encoding) + { + if (StringNullTerminatorsCache.TryGetValue(encoding, out byte[]? terminator)) + return terminator; + + terminator = encoding.GetBytes("\0"); + StringNullTerminatorsCache.TryAdd(encoding, terminator); + return terminator; + } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs index 61f25b6..1c9dfd2 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs @@ -1,5 +1,5 @@ -using System.Diagnostics; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; +using System.Text; using MindControl.Internal; using MindControl.Results; @@ -8,7 +8,67 @@ namespace MindControl; // This partial class implements the memory reading features of ProcessMemory. public partial class ProcessMemory { - #region Public methods + /// + /// Gets or sets the default maximum length of strings to read when the length is not specified. + /// The default value is a completely arbitrary 100. + /// + public int DefaultMaxStringLength { get; set; } = 100; + + #region Bytes reading + + /// + /// Reads a sequence of bytes from the address referred by the given pointer path, in the process memory. + /// + /// Optimized, reusable path to the target address. + /// Number of bytes to read. + /// The value read from the process memory, or a read failure. + public Result ReadBytes(PointerPath pointerPath, long length) + => ReadBytes(pointerPath, (ulong)length); + + /// + /// Reads a sequence of bytes from the address referred by the given pointer path, in the process memory. + /// + /// Optimized, reusable path to the target address. + /// Number of bytes to read. + /// The value read from the process memory, or a read failure. + public Result ReadBytes(PointerPath pointerPath, ulong length) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadBytes(addressResult.Value, length) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } + + /// + /// Reads a sequence of bytes from the given address in the process memory. + /// + /// Target address in the process memory. + /// Number of bytes to read. + /// The value read from the process memory, or a read failure. + public Result ReadBytes(UIntPtr address, long length) + => ReadBytes(address, (ulong)length); + + /// + /// Reads a sequence of bytes from the given address in the process memory. + /// + /// Target address in the process memory. + /// Number of bytes to read. + /// The value read from the process memory, or a read failure. + public Result ReadBytes(UIntPtr address, ulong length) + { + if (address == UIntPtr.Zero) + return new ReadFailureOnZeroPointer(); + + if (!IsBitnessCompatible(address)) + return new ReadFailureOnIncompatibleBitness(address); + + var readResult = _osService.ReadProcessMemory(_processHandle, address, length); + return readResult.IsSuccess ? readResult.Value + : new ReadFailureOnSystemRead(readResult.Error); + } + + #endregion + + #region Primitive types reading /// /// Reads a specific type of data from the address referred by the given pointer path, in the process memory. @@ -141,181 +201,257 @@ public Result Read(Type type, UIntPtr address) } } + #endregion + + #region FindStringSettings + + /// Encodings to use when trying to find the encoding of a string, in order of priority. + /// This list does not include ASCII, as there is no way to tell if a valid ASCII string is ASCII or UTF-8, + /// and we choose to prioritize UTF-8. + private static readonly Encoding[] FindStringEncodings = { Encoding.UTF8, Encoding.Latin1, Encoding.Unicode }; + + /// Length prefix sizes to try when trying to find the length prefix of a string, in order of priority. + /// + private static readonly int[] FindStringLengthPrefixSizes = { 0, 4, 2, 1, 8 }; + + /// Length prefix sizes to try when trying to find the type prefix of a string, in order of priority. + /// + private static readonly int[] FindStringTypePrefixSizes = { 0, 4, 8, 16 }; + + /// Length prefix units to try when trying to find the length prefix of a string, in order of priority. + /// + private static readonly StringLengthPrefixUnit[] FindStringLengthPrefixUnits = + { StringLengthPrefixUnit.Bytes, StringLengthPrefixUnit.Characters }; + + /// Null-termination settings to try when trying to find the null-termination of a string, in order of + /// priority. + private static readonly bool[] FindStringNullTerminated = { true, false }; + /// - /// Reads a string from the address referred by the given pointer path, in the process memory. - /// Optionally uses the given string settings and restricts the string to the specified max size. - /// If no max size is specified, a default 256 bytes value is used. - /// If no settings are specified, an attempt to guess the right settings will be made. - /// If you are getting corrupt characters in your strings, try different string settings preset or a custom string - /// settings instance. See the documentation for more information on reading strings. + /// Attempts to find appropriate settings to read and write strings at the pointer referred by the given path, + /// pointing to a known string. + /// As this is based on guesses, the settings may not be determined correctly for every possible case, but they + /// should be accurate most of the time. /// - /// Optimized, reusable path to the target address. - /// Maximal number of bytes to read as a string in memory. This value will be used as - /// a fixed size when the string settings define no null terminator and no length prefix, but will restrict the - /// number of bytes read in all cases. If not specified, a default value of 256 will be used. - /// Settings to use when attempting to read the string. Defines how the string is - /// arranged in memory (encoding, null terminators and length prefix). If you are not sure, leaving this parameter - /// to its default null value will cause an attempt to guess the right settings. If the guess does not seem to work, - /// you can try one of the presets (e.g. ), or try to figure out the details - /// and provide your own string settings. It is recommended to at least use a preset, for performance and accuracy - /// reasons. - /// The string read from memory, or a read failure. - public Result ReadString(PointerPath pointerPath, int maxSizeInBytes = 256, - StringSettings? stringSettings = null) + /// Path to a pointer that points to a known string. + /// Known string that the pointer points to. Very short strings consisting of only a + /// few characters may lead to unaccurate results. Strings containing diacritics or non-Latin characters provide + /// better results. + /// A result holding the potential string settings if they were successfully determined, or a read failure + /// otherwise. + public Result FindStringSettings(PointerPath pointerPath, + string expectedString) { var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadString(addressResult.Value, maxSizeInBytes, stringSettings) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + return addressResult.IsSuccess ? FindStringSettings(addressResult.Value, expectedString) + : new FindStringSettingsFailureOnPointerPathEvaluation(addressResult.Error); } - + /// - /// Reads a string from the given address in the process memory. - /// Optionally uses the given string settings and restricts the string to the specified max size. - /// If no max size is specified, a default 256 bytes value is used. - /// If no settings are specified, an attempt to guess the right settings will be made. - /// If you are getting corrupt characters in your strings, try different string settings preset or a custom string - /// settings instance. See the documentation for more information on reading strings. + /// Attempts to find appropriate settings to read and write strings at the given address pointing to a known string. + /// As this is based on guesses, the settings may not be determined correctly for every possible case, but they + /// should be accurate most of the time. /// - /// Target address in the process memory. - /// Maximal number of bytes to read as a string in memory. This value will be used as - /// a fixed size when the string settings define no null terminator and no length prefix, but will restrict the - /// number of bytes read in all cases. If not specified, a default value of 256 will be used. - /// Settings to use when attempting to read the string. Defines how the string is - /// arranged in memory (encoding, null terminators and length prefix). If you are not sure, leaving this parameter - /// to its default null value will cause an attempt to guess the right settings. If the guess does not seem to work, - /// you can try one of the presets (e.g. ), or try to figure out the details - /// and provide your own string settings. It is recommended to at least use a preset, for performance and accuracy - /// reasons. - /// The string read from memory, or a read failure. - public Result ReadString(UIntPtr address, int maxSizeInBytes = 256, - StringSettings? stringSettings = null) + /// Address to a pointer that points to a known string. + /// Known string that the pointer points to. Very short strings consisting of only a + /// few characters may lead to unaccurate results. Strings containing diacritics or non-Latin characters provide + /// better results. + /// A result holding the potential string settings if they were successfully determined, or a read failure + /// otherwise. + public Result FindStringSettings(UIntPtr stringPointerAddress, + string expectedString) { - var actualStringSettings = stringSettings ?? GuessStringSettings(); - var lengthToRead = (ulong)maxSizeInBytes; + if (string.IsNullOrWhiteSpace(expectedString)) + return new FindStringSettingsFailureOnNoSettingsFound(); + + // We have to determine 4 parameters: + // - The encoding of the string + // - The length prefix settings (an optional prefix that holds the length of the string) + // - The type prefix (an optional prefix that comes before the string bytes, usually a type pointer) + // - Whether the string is null-terminated + + // We can attempt to pinpoint all these through trial and error, because we know the expected string. + // We will try to read the string with different setting combinations in a specific order until we read the + // expected string. - if (actualStringSettings.PrefixSettings != null) + // Read the address of the actual string from the pointer + var pointerResult = Read(stringPointerAddress); + if (pointerResult.IsFailure) + return new FindStringSettingsFailureOnPointerReadFailure(pointerResult.Error); + + var pointerValue = pointerResult.Value; + if (pointerValue == UIntPtr.Zero) + return new FindStringSettingsFailureOnZeroPointer(); + + // Read the max amount of bytes we might need to read the string from + // This is computed from the sum of the longest considered type prefix, the longest considered length prefix, + // the max byte count of the most complex encoding considered, and the longest considered null terminator. + int maxBytesFromRawString = FindStringEncodings.Max(e => e.GetMaxByteCount(expectedString.Length)); + int maxBytesFromLengthPrefix = FindStringLengthPrefixSizes.Max(); + int maxBytesFromTypePrefix = FindStringTypePrefixSizes.Max(); + int maxBytesFromNullTerminator = FindStringEncodings.Max(e => e.GetNullTerminator().Length); + int maxBytesTotal = maxBytesFromRawString + maxBytesFromLengthPrefix + maxBytesFromTypePrefix + + maxBytesFromNullTerminator; + + var bytesResult = ReadBytes(pointerValue, (ulong)maxBytesTotal); + if (bytesResult.IsFailure) + return new FindStringSettingsFailureOnStringReadFailure(bytesResult.Error); + byte[] bytes = bytesResult.Value; + + // Iterate through all possible setting combinations from the considered setting values + // Note that the order of the loops is important, as it determines the priority of the settings. + // The order of values in the arrays is also important, as it determines the priority of the values. + // This is important not only for performance, but also for accuracy (to return the most probable match first, + // even when multiple combinations work). + // For instance, the type prefix loop comes first, and should have a value of 0 at first, in order to prevent + // false positives where the type prefix wrongly contains the length prefix. + foreach (int typePrefixSize in FindStringTypePrefixSizes) { - var lengthPrefixBytesResult = ReadBytes(address, actualStringSettings.PrefixSettings.PrefixSize); - - if (lengthPrefixBytesResult.IsFailure) - return lengthPrefixBytesResult.Error; - - byte[] lengthPrefixBytes = lengthPrefixBytesResult.Value; - - // Automatically determine the length unit if not provided: - // Should be the minimal number of bytes supported by the encoding for a single character. - // To get that, we make the encoding output the bytes for the string "a" and count the bytes. - var lengthUnit = actualStringSettings.PrefixSettings.LengthUnit - ?? actualStringSettings.Encoding.GetBytes("a").Length; - ulong lengthPrefixValue = lengthPrefixBytes.ReadUnsignedNumber() * (ulong)lengthUnit; - - lengthToRead = Math.Min(lengthToRead, lengthPrefixValue); - address += actualStringSettings.PrefixSettings.PrefixSize; + foreach (var encoding in FindStringEncodings) + { + foreach (int lengthPrefixSize in FindStringLengthPrefixSizes) + { + foreach (var lengthPrefixUnit in FindStringLengthPrefixUnits) + { + // Avoid going multiple times through the same length prefix settings + if (lengthPrefixSize == 0 && lengthPrefixUnit != FindStringLengthPrefixUnits.First()) + break; + + foreach (bool isNullTerminated in FindStringNullTerminated) + { + // Skip the case where the string is null-terminated but has no length prefix, as this is + // not a valid setting. + if (!isNullTerminated && lengthPrefixSize == 0) + continue; + + // Build a settings instance with the current combination + var settings = new StringSettings(encoding) + { + IsNullTerminated = isNullTerminated, + LengthPrefix = lengthPrefixSize > 0 ? new StringLengthPrefix(lengthPrefixSize, + lengthPrefixUnit) : null, + TypePrefix = typePrefixSize > 0 ? bytes.Take(typePrefixSize).ToArray() : null + }; + + // Try to read the string with the current settings + string? result = settings.GetString(bytes); + + // We have a match if the result matches the expected string. + // Otherwise, we continue with the next settings. + if (result == expectedString) + return settings; + } + } + } + } } - - // Read the actual bytes on the full length - var stringBytesResult = ReadBytes(address, lengthToRead); - if (!stringBytesResult.IsSuccess) - return stringBytesResult.Error; - byte[] bytes = stringBytesResult.Value; - - // Convert the whole byte array to a string - string fullString = actualStringSettings.Encoding.GetString(bytes); - // Cut it to the first null terminator if the settings allow it - return actualStringSettings.IsNullTerminated ? fullString.Split('\0')[0] : fullString; + // If we reach this point, we could not find any settings that would allow us to read the expected string. + return new FindStringSettingsFailureOnNoSettingsFound(); } + + #endregion + + #region String reading /// - /// Reads a sequence of bytes from the address referred by the given pointer path, in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Number of bytes to read. - /// The value read from the process memory, or a read failure. - public Result ReadBytes(PointerPath pointerPath, long length) - => ReadBytes(pointerPath, (ulong)length); - - /// - /// Reads a sequence of bytes from the address referred by the given pointer path, in the process memory. + /// Reads a string from the address referred by the given pointer path, in the process memory. + /// The address must point to the start of the actual string bytes. Consider to + /// read strings from pointers and with added capabilities. + /// Read the documentation for more information. /// - /// Optimized, reusable path to the target address. - /// Number of bytes to read. - /// The value read from the process memory, or a read failure. - public Result ReadBytes(PointerPath pointerPath, ulong length) + /// Path to the first byte of the raw string in the process memory. + /// Encoding of the string to use when decoding. Try changing this parameter if you get + /// garbage characters or empty strings. Common values include and + /// . + /// + /// Maximum length of the string to read, in characters. If left null (default), the + /// will be used. + /// Boolean indicating if the string is null-terminated. If true, the string will be + /// read until the first null character. If false, the string will be read up to the maximum length specified. + /// + /// The string read from the process memory, or a read failure. + public Result ReadRawString(PointerPath pointerPath, Encoding encoding, + int? maxLength = null, bool isNullTerminated = true) { var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadBytes(addressResult.Value, length) + return addressResult.IsSuccess ? ReadRawString(addressResult.Value, encoding, maxLength, isNullTerminated) : new ReadFailureOnPointerPathEvaluation(addressResult.Error); } /// - /// Reads a sequence of bytes from the given address in the process memory. - /// - /// Target address in the process memory. - /// Number of bytes to read. - /// The value read from the process memory, or a read failure. - public Result ReadBytes(UIntPtr address, long length) - => ReadBytes(address, (ulong)length); - - /// - /// Reads a sequence of bytes from the given address in the process memory. + /// Reads a string from the given address in the process memory. + /// The address must point to the start of the actual string bytes. Consider to + /// read strings from pointers and with added capabilities. + /// Read the documentation for more information. /// - /// Target address in the process memory. - /// Number of bytes to read. - /// The value read from the process memory, or a read failure. - public Result ReadBytes(UIntPtr address, ulong length) + /// Address of the first byte of the raw string in the process memory. + /// Encoding of the string to use when decoding. Try changing this parameter if you get + /// garbage characters or empty strings. Common values include and + /// . + /// + /// Maximum length of the string to read, in characters. If left null (default), the + /// will be used. + /// Boolean indicating if the string is null-terminated. If true, the string will be + /// read until the first null character. If false, the string will be read up to the maximum length specified. + /// + /// The string read from the process memory, or a read failure. + public Result ReadRawString(UIntPtr address, Encoding encoding, + int? maxLength = null, bool isNullTerminated = true) { - if (address == UIntPtr.Zero) - return new ReadFailureOnZeroPointer(); - - if (!IsBitnessCompatible(address)) - return new ReadFailureOnIncompatibleBitness(address); + if (maxLength is < 0) + throw new ArgumentOutOfRangeException(nameof(maxLength), "The maximum length cannot be negative."); - var readResult = _osService.ReadProcessMemory(_processHandle, address, length); - return readResult.IsSuccess ? readResult.Value - : new ReadFailureOnSystemRead(readResult.Error); + // We don't know how many bytes the string will take, because encodings can have variable byte sizes. + // So we read the maximum amount of bytes that the string could take, and then cut it to the max length. + + // Calculate the maximum byte size to read + maxLength = maxLength ?? DefaultMaxStringLength; + int byteSizeToRead = encoding.GetMaxByteCount(maxLength.Value) + + (isNullTerminated ? encoding.GetNullTerminator().Length : 0); + + // Read the bytes + //todo: Use a read method that handles cases where bytes are only partially read, because we don't want the + //operation to fail if we read a string that's at the end of a region and the next region is not readable. + var readResult = ReadBytes(address, (ulong)byteSizeToRead); + if (readResult.IsFailure) + return readResult.Error; + + // Convert the bytes to a string + byte[] bytes = readResult.Value; + string? result = new StringSettings(encoding, isNullTerminated).GetString(bytes); + + // Cut the string to the max length if needed + if (result?.Length > maxLength) + result = result[..maxLength.Value]; + + return result ?? string.Empty; } - - #endregion - - #region Utility - + /// - /// Attempts to guess what string settings to use to read from or write into this process. + /// Reads the string pointed by the pointer at the given address from the process memory. + /// This method uses a instance to determine how to read the string. /// - private StringSettings GuessStringSettings() + /// Address of the pointer to the string in the process memory. + /// Settings that define how to read the string. If you cannot figure out what settings to + /// use, try to automatically determine the right settings for a + /// known string pointer. See the documentation for more information. + /// The string read from the process memory, or a read failure. + public Result ReadStringPointer(UIntPtr address, StringSettings settings) { - // The idea is to try and find out what language/framework the process was compiled from, by looking for - // specific markers of that language/framework. - // If we can figure that out, we can pick the string setting preset that seems more likely. - // For instance, in .net programs, strings will be UTF-16, with a prefixed length and null terminator. - // If we can't figure anything out, we'll just return the default string settings. - // This method is intended to provide a rough "guesstimation" that should work in as many cases as possible, as - // an attempt to simplify things for hacking beginners who might not be able to figure out what their string - // settings should be. It is designed to only be called when the user doesn't provide a string setting. + // The string settings will either have a null terminator or a length prefix. + // The reading strategy depends on which of these two we have. - var moduleNames = _process.Modules - .Cast() - .Select(m => m.ModuleName?.ToLowerInvariant()) - .Where(m => m != null) - .Cast() - .ToHashSet(); + // If we have a null terminator, read small chunks of bytes until we find a null terminator. + // For this, we may need to make an alternative way to read bytes that returns a custom stream. The stream + // would end whenever we reach unreadable memory. + // In this method, we would read from the stream until we find a null terminator. + // Multi-bytes null terminators could be handled this way too, because if we get a sequence of 2 null bytes, + // we know that the null terminator is either at the start of the sequence, or the start of the sequence + 1. - if (moduleNames.Contains("java.exe")) - return StringSettings.Java; - - if (moduleNames.Contains("clrjit.dll") - || moduleNames.Contains("unityplayer.dll") - || moduleNames.Any(m => m.StartsWith("mono-"))) - return StringSettings.DotNet; + // If we have a length prefix, read the length prefix and then read the string bytes at once. - // Any additional language/framework detection can be added here. - // Be aware that this method might be called frequently. Mind the performance cost. - // Implementing a cache might be a good idea if we need to analyze symbols or costly stuff like that. - - // If we're not in any of the specifically identified cases, return the catch-all default settings. - // This setting might very well not work at all with the process, but we've done our best! - return StringSettings.Default; + throw new NotImplementedException(); } #endregion diff --git a/src/MindControl/Results/FindStringSettingsFailure.cs b/src/MindControl/Results/FindStringSettingsFailure.cs new file mode 100644 index 0000000..11de3ae --- /dev/null +++ b/src/MindControl/Results/FindStringSettingsFailure.cs @@ -0,0 +1,102 @@ +namespace MindControl.Results; + +/// +/// Represents a reason for a string settings search operation to fail. +/// +public enum FindStringSettingsFailureReason +{ + /// + /// Failure when trying to evaluate the given pointer path. + /// + PointerPathEvaluation, + + /// + /// Failure when trying to read the given pointer. + /// + PointerReadFailure, + + /// + /// The given pointer is a zero pointer. + /// + ZeroPointer, + + /// + /// Failure when trying to read bytes at the address pointed by the given pointer. + /// + StringReadFailure, + + /// + /// No adequate settings were found to read the given string from the specified pointer. + /// + NoSettingsFound +} + +/// +/// Represents a failure in a string settings search operation. +/// +/// Reason for the failure. +public abstract record FindStringSettingsFailure(FindStringSettingsFailureReason Reason); + +/// +/// Represents a failure in a string settings search operation when failing to evaluate the specified pointer path. +/// +/// Underlying path evaluation failure details. +public record FindStringSettingsFailureOnPointerPathEvaluation(PathEvaluationFailure Details) + : FindStringSettingsFailure(FindStringSettingsFailureReason.PointerPathEvaluation) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"Failed to evaluate the specified pointer path: {Details}"; +} + +/// +/// Represents a failure in a string settings search operation when failing to read the value of the given pointer. +/// +/// Underlying read failure details. +public record FindStringSettingsFailureOnPointerReadFailure(ReadFailure Details) + : FindStringSettingsFailure(FindStringSettingsFailureReason.PointerReadFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"Failed to read a pointer while searching for string settings: {Details}"; +} + +/// +/// Represents a failure in a string settings search operation when the given pointer is a zero pointer. +/// +public record FindStringSettingsFailureOnZeroPointer() + : FindStringSettingsFailure(FindStringSettingsFailureReason.ZeroPointer) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => "The given pointer is a zero pointer."; +} + +/// +/// Represents a failure in a string settings search operation when failing to read bytes at the address pointed by the +/// given pointer. +/// +/// Underlying read failure details. +public record FindStringSettingsFailureOnStringReadFailure(ReadFailure Details) + : FindStringSettingsFailure(FindStringSettingsFailureReason.StringReadFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"Failed to read bytes at the address pointed by the given pointer while searching for string settings: {Details}"; +} + +/// +/// Represents a failure in a string settings search operation when no adequate settings were found to read the given +/// string from the specified pointer. +/// +public record FindStringSettingsFailureOnNoSettingsFound() + : FindStringSettingsFailure(FindStringSettingsFailureReason.NoSettingsFound) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => "No adequate settings were found to read the given string from the specified pointer."; +} diff --git a/src/MindControl/Results/PathEvaluationFailure.cs b/src/MindControl/Results/PathEvaluationFailure.cs index ad3892c..df006d4 100644 --- a/src/MindControl/Results/PathEvaluationFailure.cs +++ b/src/MindControl/Results/PathEvaluationFailure.cs @@ -81,12 +81,12 @@ public override string ToString() /// Represents a failure in a path evaluation operation when invoking the system API to read an address. /// /// Address that caused the failure. -/// Details about the failure. -public record PathEvaluationFailureOnPointerReadFailure(UIntPtr Address, ReadFailure Failure) +/// Details about the failure. +public record PathEvaluationFailureOnPointerReadFailure(UIntPtr Address, ReadFailure Details) : PathEvaluationFailure(PathEvaluationFailureReason.PointerReadFailure) { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"Failed to read a pointer at the address {Address}: {Failure}"; + => $"Failed to read a pointer at the address {Address}: {Details}"; } \ No newline at end of file diff --git a/src/MindControl/Results/ReadFailure.cs b/src/MindControl/Results/ReadFailure.cs index 72e7091..e39dc2c 100644 --- a/src/MindControl/Results/ReadFailure.cs +++ b/src/MindControl/Results/ReadFailure.cs @@ -45,14 +45,14 @@ public abstract record ReadFailure(ReadFailureReason Reason); /// /// Represents a failure in a memory read operation when evaluating the pointer path to the target memory. /// -/// Details about the failure. -public record ReadFailureOnPointerPathEvaluation(PathEvaluationFailure PathEvaluationFailure) +/// Details about the failure. +public record ReadFailureOnPointerPathEvaluation(PathEvaluationFailure Details) : ReadFailure(ReadFailureReason.PointerPathEvaluationFailure) { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"Failed to evaluate the specified pointer path: {PathEvaluationFailure}"; + => $"Failed to evaluate the specified pointer path: {Details}"; } /// @@ -84,14 +84,14 @@ public override string ToString() /// /// Represents a failure in a memory read operation when invoking the system API to read the target memory. /// -/// Details about the failure. -public record ReadFailureOnSystemRead(SystemFailure SystemReadFailure) +/// Details about the failure. +public record ReadFailureOnSystemRead(SystemFailure Details) : ReadFailure(ReadFailureReason.SystemReadFailure) { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"Failed to read at the target address: {SystemReadFailure}"; + => $"Failed to read at the target address: {Details}"; } /// diff --git a/src/MindControl/Results/WriteFailure.cs b/src/MindControl/Results/WriteFailure.cs index 03cfc48..8370e0a 100644 --- a/src/MindControl/Results/WriteFailure.cs +++ b/src/MindControl/Results/WriteFailure.cs @@ -51,14 +51,14 @@ public abstract record WriteFailure(WriteFailureReason Reason); /// /// Represents a failure in a memory write operation when evaluating the pointer path to the target memory. /// -/// Details about the failure. -public record WriteFailureOnPointerPathEvaluation(PathEvaluationFailure PathEvaluationFailure) +/// Details about the failure. +public record WriteFailureOnPointerPathEvaluation(PathEvaluationFailure Details) : WriteFailure(WriteFailureReason.PointerPathEvaluationFailure) { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"Failed to evaluate the specified pointer path: {PathEvaluationFailure}"; + => $"Failed to evaluate the specified pointer path: {Details}"; } /// @@ -91,14 +91,14 @@ public record WriteFailureOnZeroPointer() /// the target memory space fails. /// /// Address where the operation failed. -/// Details about the failure. -public record WriteFailureOnSystemProtectionRemoval(UIntPtr Address, SystemFailure Failure) +/// Details about the failure. +public record WriteFailureOnSystemProtectionRemoval(UIntPtr Address, SystemFailure Details) : WriteFailure(WriteFailureReason.SystemProtectionRemovalFailure) { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"Failed to remove the protection of the memory at address {Address}: {Failure}.{Environment.NewLine}Change the memory protection strategy to {nameof(MemoryProtectionStrategy)}.{nameof(MemoryProtectionStrategy.Ignore)} to prevent memory protection removal. As protection removal is the first step when writing a value, it may simply be that the provided target address does not point to valid memory."; + => $"Failed to remove the protection of the memory at address {Address}: {Details}.{Environment.NewLine}Change the memory protection strategy to {nameof(MemoryProtectionStrategy)}.{nameof(MemoryProtectionStrategy.Ignore)} to prevent memory protection removal. As protection removal is the first step when writing a value, it may simply be that the provided target address does not point to valid memory."; } /// @@ -106,26 +106,26 @@ public override string ToString() /// the target memory space after writing fails. /// /// Address where the operation failed. -/// Details about the failure. -public record WriteFailureOnSystemProtectionRestoration(UIntPtr Address, SystemFailure Failure) +/// Details about the failure. +public record WriteFailureOnSystemProtectionRestoration(UIntPtr Address, SystemFailure Details) : WriteFailure(WriteFailureReason.SystemProtectionRestorationFailure) { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"The value was written successfully, but the protection of the memory at address {Address} could not be restored to its original value: {Failure}.{Environment.NewLine}Change the memory protection strategy to {nameof(MemoryProtectionStrategy)}.{nameof(MemoryProtectionStrategy.Remove)} to prevent memory protection restoration."; + => $"The value was written successfully, but the protection of the memory at address {Address} could not be restored to its original value: {Details}.{Environment.NewLine}Change the memory protection strategy to {nameof(MemoryProtectionStrategy)}.{nameof(MemoryProtectionStrategy.Remove)} to prevent memory protection restoration."; } /// /// Represents a failure in a memory write operation when the system API call to write bytes in memory fails. /// /// Address where the write operation failed. -/// Details about the failure. -public record WriteFailureOnSystemWrite(UIntPtr Address, SystemFailure Failure) +/// Details about the failure. +public record WriteFailureOnSystemWrite(UIntPtr Address, SystemFailure Details) : WriteFailure(WriteFailureReason.SystemWriteFailure) { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"Failed to write at the address {Address}: {Failure}"; + => $"Failed to write at the address {Address}: {Details}"; } diff --git a/src/MindControl/StringSettings.cs b/src/MindControl/StringSettings.cs index c26a818..728a5ca 100644 --- a/src/MindControl/StringSettings.cs +++ b/src/MindControl/StringSettings.cs @@ -1,4 +1,6 @@ -using System.Text; +using System.Buffers.Binary; +using System.Text; +using MindControl.Internal; namespace MindControl; @@ -7,41 +9,12 @@ namespace MindControl; /// public class StringSettings { - #region Presets - /// - /// Default string settings preset (null-terminated UTF-16 without length prefix). This may not work well for your - /// process. Please use different settings if you notice corrupt characters in your strings. + /// Gets or sets an optional prefix that comes before the string bytes. This is useful for type pointers in + /// frameworks that use them. If the string also has a length prefix, the type prefix comes first, before the + /// length. /// - public static readonly StringSettings Default = new(Encoding.Unicode, true, null); - - /// - /// An alternative to the default string settings preset using the equally common UTF-8 instead of UTF-16. Try this - /// preset if the default one does not work for your process. - /// Please try different settings if you notice corrupt characters in your strings. - /// - public static readonly StringSettings DefaultUtf8 = new(Encoding.UTF8, true, null); - - /// - /// String settings preset for .net processes (UTF-16 with both length prefix and null terminator). Be aware that a - /// process may hold strings that use different settings. - /// - public static readonly StringSettings DotNet = new(Encoding.Unicode, true, new StringLengthPrefixSettings(4, 2)); - - /// - /// String setting preset for Java processes (UTF-16 with length prefix, no null terminator). Be aware that a - /// process may hold strings that use different settings. In particular, under some circumstances, Java may use the - /// ISO-8859-1 encoding for some strings to optimize performance. - /// - public static readonly StringSettings Java = new(Encoding.Unicode, false, new StringLengthPrefixSettings(4, 2)); - - /// - /// String setting preset for Rust processes (UTF-8 with length prefix, no null terminator). Be aware that a - /// process may hold strings that use different settings. - /// - public static readonly StringSettings Rust = new(Encoding.UTF8, false, new StringLengthPrefixSettings(2, 1)); - - #endregion + public byte[]? TypePrefix { get; set; } /// /// Gets or sets a boolean indicating if strings should have a \0 delimitation character at the end. @@ -57,77 +30,260 @@ public class StringSettings /// Gets or sets the length prefix settings. /// If null, strings are considered to have no length prefix. /// - public StringLengthPrefixSettings? PrefixSettings { get; set; } + public StringLengthPrefix? LengthPrefix { get; set; } + /// + /// Gets a boolean indicating if the settings are valid. + /// + public bool IsValid => IsNullTerminated || LengthPrefix != null; + /// /// Builds settings with the given properties. + /// If you are unsure what settings to use, consider using to + /// automatically determine the appropriate settings for a known string pointer. /// /// Encoding of the strings. /// Boolean indicating if strings should have a \0 delimitation character at the end. - /// - /// Length prefix settings. If null, strings are considered to have no length prefix. - /// - public StringSettings(Encoding encoding, bool isNullTerminated, StringLengthPrefixSettings? prefixSettings) + /// The default value is true. + /// Length prefix settings. If null, strings are considered to have no length prefix. + /// The default value is null. + /// Optional prefix that comes before the string bytes. This is useful for type pointers in + /// frameworks that use them. If the string also has a length prefix, the type prefix comes first, before the + /// length. The default value is null. + public StringSettings(Encoding encoding, bool isNullTerminated = true, + StringLengthPrefix? lengthPrefix = null, + byte[]? typePrefix = null) { Encoding = encoding; IsNullTerminated = isNullTerminated; - PrefixSettings = prefixSettings; + LengthPrefix = lengthPrefix; + TypePrefix = typePrefix; } /// - /// Builds settings with the given properties. With this constructor, strings are considered to have no length - /// prefix. + /// Throws an exception if the settings are invalid. /// - /// Encoding of the strings. - /// Boolean indicating if strings should have a \0 delimitation character at the end. - /// - public StringSettings(Encoding encoding, bool isNullTerminated) : this(encoding, isNullTerminated, null) {} + /// Thrown when the settings are not valid. + protected void ThrowIfInvalid() + { + if (!IsValid) + throw new InvalidOperationException( + "The settings are not valid. Either a length prefix or null terminator is required."); + } + + /// + /// Computes the number of bytes to read when reading a string with these settings, given a maximum string length. + /// + /// Maximum length of the string to read. + /// The number of bytes to read. + public int GetMaxByteLength(int maxStringLength) + { + ThrowIfInvalid(); + + return Encoding.GetMaxByteCount(maxStringLength) + + (LengthPrefix?.PrefixSize ?? 0) + + (TypePrefix?.Length ?? 0) + + (IsNullTerminated ? Encoding.GetNullTerminator().Length : 0); + } + + /// + /// Converts the given string into a byte array using these settings. May fail and return null when using a length + /// prefix that is too small to represent the string. + /// + /// String to convert. + /// The byte array representing the string, or null if the string is too long to be represented with the + /// length prefix setting. + public byte[]? GetBytes(string value) + { + ThrowIfInvalid(); + + // Note: this method is optimized to avoid unnecessary allocations. + // Because of this, we cannot use certain methods like Encoding.GetBytes. + + // Check if the string is too long to be represented with the length prefix + // We do this first to avoid allocating memory when the string is too long + int byteCount = Encoding.GetByteCount(value); + int? lengthToWrite = null; + if (LengthPrefix != null) + { + lengthToWrite = LengthPrefix.Unit switch + { + StringLengthPrefixUnit.Characters => value.Length, + _ => byteCount + }; + + switch (LengthPrefix.PrefixSize) + { + case 1 when lengthToWrite > byte.MaxValue: + case 2 when lengthToWrite > short.MaxValue: + return null; + } + } + + // Avoid unnecessary allocations. Only allocate a single byte array + var bytes = new byte[GetByteCount(value)]; + + // Write the type prefix + int currentIndex = 0; + TypePrefix?.CopyTo(bytes, 0); + currentIndex += TypePrefix?.Length ?? 0; + + // Write the length prefix + if (lengthToWrite != null) + { + // Write the length into the byte array, after the type prefix if any + var span = bytes.AsSpan(currentIndex); + if (LengthPrefix!.PrefixSize == 1) + span[0] = (byte)lengthToWrite.Value; + else if (LengthPrefix.PrefixSize == 2) + BinaryPrimitives.WriteInt16LittleEndian(span, (short)lengthToWrite.Value); + else if (LengthPrefix.PrefixSize == 4) + BinaryPrimitives.WriteInt32LittleEndian(span, lengthToWrite.Value); + else + BinaryPrimitives.WriteInt64LittleEndian(span, lengthToWrite.Value); + + currentIndex += LengthPrefix.PrefixSize; + } + + // Write the string bytes. Encoder.Convert is used to avoid unnecessary allocations. + Encoding.GetEncoder().Convert(value, bytes.AsSpan(currentIndex, byteCount), true, out _, out _, out _); + currentIndex += byteCount; + + // Write the null terminator if needed + if (IsNullTerminated) + Encoding.GetNullTerminator().CopyTo(bytes, currentIndex); + + return bytes; + } /// - /// Builds settings with the given properties. With this constructor, strings are considered to have no length - /// prefix, and to be null-terminated. + /// Gets the number of bytes that a specific string would occupy in memory with these settings. /// - /// Encoding of the strings. - public StringSettings(Encoding encoding) : this(encoding, true, null) {} + /// String to measure. + /// The number of bytes that the string would occupy in memory. + public int GetByteCount(string value) + { + ThrowIfInvalid(); + + return (TypePrefix?.Length ?? 0) + + (LengthPrefix?.PrefixSize ?? 0) + + Encoding.GetByteCount(value) + + (IsNullTerminated ? Encoding.GetNullTerminator().Length : 0); + } + + /// + /// Attempts to read a string from the given bytes with this settings instance. + /// + /// Bytes to read the string from. + /// The string read from the bytes, or null if the string could not be read. + public string? GetString(byte[] bytes) + { + // Figure out the start index of the actual string bytes, after the prefixes (if any) + int lengthPrefixSize = LengthPrefix?.PrefixSize ?? 0; + int typePrefixSize = TypePrefix?.Length ?? 0; + int startIndex = lengthPrefixSize + typePrefixSize; + + // Calculate the remaining bytes to read + int remainingBytes = bytes.Length - startIndex; + if (remainingBytes <= 0) + return string.Empty; + + // Calculate how many bytes we have to read. + // If the length prefix is in bytes, read the length prefix and use it as the length to read. + // If we have a null terminator, we also have to read it, so add its length to the length to read. + // Otherwise, read the remaining bytes. + bool hasBytesLengthPrefix = LengthPrefix is { PrefixSize: > 0, Unit: StringLengthPrefixUnit.Bytes }; + ulong lengthToRead = hasBytesLengthPrefix ? + bytes.AsSpan(typePrefixSize, lengthPrefixSize).ReadUnsignedNumber() + + (ulong)(IsNullTerminated ? Encoding.GetNullTerminator().Length : 0) + : (ulong)remainingBytes; + + // Check if we have enough bytes to read the string. + // This will fail if we read a byte length prefix that is too large. + // In that case, return null, as this means the string settings don't work with the input bytes. + if ((ulong)remainingBytes < lengthToRead) + return null; + + // Check the bounds of the length to read + if (lengthToRead is 0 or > int.MaxValue) + return string.Empty; + + // Read the string bytes using the encoding in the settings + var stringBytes = bytes.AsSpan(startIndex, (int)lengthToRead); + if (stringBytes.Length == 0) + return null; + string resultingString = Encoding.GetString(stringBytes); + + // If we have a length prefix in characters, we have to cut the string to the correct length + if (LengthPrefix is { PrefixSize: > 0, Unit: StringLengthPrefixUnit.Characters }) + { + ulong characterLength = bytes.AsSpan(typePrefixSize, lengthPrefixSize).ReadUnsignedNumber(); + if (IsNullTerminated) + characterLength++; + + if (characterLength == (ulong)resultingString.Length) + return resultingString; + if (characterLength > (ulong)resultingString.Length) + return null; + resultingString = resultingString[..(int)characterLength]; + } + + // If the string is null-terminated, we have to cut the string at the null-terminator + // If there is no null-terminator, return null, as this means the string settings and byte array are not + // compatible. + if (IsNullTerminated) + { + int nullTerminatorIndex = resultingString.IndexOf('\0'); + return nullTerminatorIndex >= 0 ? resultingString[..nullTerminatorIndex] : null; + } + + // No length prefix, or length prefix in bytes. Return the full string. + return resultingString; + } +} + +/// +/// Defines what is counted by a string length prefix. +/// +public enum StringLengthPrefixUnit +{ + /// + /// The length prefix is a count of characters. + /// + Characters, + + /// + /// The length prefix is a count of bytes. + /// + Bytes } /// /// Defines settings about the prefix that holds the length of a string. /// -public class StringLengthPrefixSettings +public class StringLengthPrefix { /// - /// Gets or sets the number of bytes to read or write as a length prefix before strings. + /// Gets or sets the number of bytes storing the length of the string. /// - public int PrefixSize { get; set; } + public int PrefixSize { get; } /// - /// Gets or sets the number of bytes counted by the length prefix. - /// For example, in a read operation, if this value is 2 and the length prefix evaluates to 21, the string will be - /// read as 42 bytes. - /// If null, this value will be automatically determined using the encoding. + /// Gets or sets what the length prefix counts. /// - public int? LengthUnit { get; set; } + public StringLengthPrefixUnit Unit { get; } /// /// Builds length prefix settings with the given properties. /// - /// Number of bytes to read or write as a length prefix before strings. - /// Number of bytes counted by the length prefix. Automatically determined if set to null. - /// - public StringLengthPrefixSettings(int prefixSize, int? lengthUnit) + /// Number of bytes storing the length of the string. + /// What the length prefix counts. + public StringLengthPrefix(int prefixSize, StringLengthPrefixUnit unit) { - if (prefixSize is > 8 or < 1) - throw new ArgumentOutOfRangeException(nameof(prefixSize), "Prefix size must be between 1 and 8 included."); + if (prefixSize != 1 && prefixSize != 2 && prefixSize != 4 && prefixSize != 8) + throw new ArgumentOutOfRangeException(nameof(prefixSize), "Prefix size must be either 1, 2, 4 or 8."); PrefixSize = prefixSize; - LengthUnit = lengthUnit; + Unit = unit; } - - /// - /// Builds length prefix settings with the given properties. - /// With this constructor, the length unit is set to be determined automatically. - /// - /// Number of bytes to read or write as a length prefix before strings. - public StringLengthPrefixSettings(int prefixSize) : this(prefixSize, null) {} } diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs index dea18c2..c206492 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs @@ -32,9 +32,6 @@ public void EvaluateOnKnownPointerTest() [Test] public void EvaluateOverMaxPointerValueTest() { - var expectedPreviousAddress = TestProcessMemory!.EvaluateMemoryAddress($"{OuterClassPointer:X}+10,10") - .GetValueOrDefault(); - var result = TestProcessMemory!.EvaluateMemoryAddress($"{OuterClassPointer:X}+10,10,1"); Assert.That(result.IsSuccess, Is.False); @@ -60,10 +57,10 @@ public void EvaluateWithUnreadableAddressTest() Assert.That(error, Is.TypeOf()); var pathError = (PathEvaluationFailureOnPointerReadFailure)error; Assert.That(pathError.Address, Is.EqualTo((UIntPtr)ulong.MaxValue)); - Assert.That(pathError.Failure, Is.TypeOf()); - var readFailure = (ReadFailureOnSystemRead)pathError.Failure; - Assert.That(readFailure.SystemReadFailure, Is.TypeOf()); - var osFailure = (OperatingSystemCallFailure)readFailure.SystemReadFailure; + Assert.That(pathError.Details, Is.TypeOf()); + var readFailure = (ReadFailureOnSystemRead)pathError.Details; + Assert.That(readFailure.Details, Is.TypeOf()); + var osFailure = (OperatingSystemCallFailure)readFailure.Details; Assert.That(osFailure.ErrorCode, Is.GreaterThan(0)); Assert.That(osFailure.ErrorMessage, Is.Not.Empty); } diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index a05a703..ea3a4af 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -10,8 +10,7 @@ namespace MindControl.Test.ProcessMemoryTests; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ProcessMemoryReadTest : ProcessMemoryTest { - /// A struct with a couple fields, to test reading structs. - public record struct TestStruct(double A, int B); + #region Primitive type reading /// /// Tests . @@ -184,38 +183,6 @@ public void ReadDoubleTest() Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(-99879416311.4478)); } - /// - /// Tests . - /// Reads a known struct from the target process before and after the process changes their value. - /// It should be equal to its known values before and after modification. - /// - [Test] - public void ReadStructureTest() - { - var pointerPath = new PointerPath($"{OuterClassPointer:X}+30"); - Assert.That(TestProcessMemory!.Read(pointerPath).Value, - Is.EqualTo(new TestStruct(79879131651.33345, -7651))); - ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(pointerPath).Value, - Is.EqualTo(new TestStruct(-99879416311.4478, 987411))); - } - - /// - /// Tests . - /// Reads a known struct from the target process before and after the process changes their value. - /// It should be equal to its known values before and after modification. - /// - [Test] - public void ReadStructureAsObjectTest() - { - var pointerPath = new PointerPath($"{OuterClassPointer:X}+30"); - Assert.That(TestProcessMemory!.Read(typeof(TestStruct), pointerPath).Value, - Is.EqualTo(new TestStruct(79879131651.33345, -7651))); - ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(typeof(TestStruct), pointerPath).Value, - Is.EqualTo(new TestStruct(-99879416311.4478, 987411))); - } - /// /// Tests . /// Reads a known byte array from the target process before and after the process changes their value. @@ -269,7 +236,7 @@ public void ReadAtMaxPointerValueTest() Assert.That(result.IsSuccess, Is.False); var error = result.Error; Assert.That(error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); - var systemError = ((ReadFailureOnSystemRead)error).SystemReadFailure; + var systemError = ((ReadFailureOnSystemRead)error).Details; Assert.That(systemError, Is.TypeOf(typeof(OperatingSystemCallFailure))); var osError = (OperatingSystemCallFailure)systemError; Assert.That(osError.ErrorCode, Is.GreaterThan(0)); @@ -288,7 +255,7 @@ public void ReadOverMaxPointerValueTest() Assert.That(result.IsSuccess, Is.False); var error = result.Error; Assert.That(error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); - var pathError = ((ReadFailureOnPointerPathEvaluation)error).PathEvaluationFailure; + var pathError = ((ReadFailureOnPointerPathEvaluation)error).Details; Assert.That(pathError, Is.TypeOf(typeof(PathEvaluationFailureOnPointerOutOfRange))); var outOfRangeError = (PathEvaluationFailureOnPointerOutOfRange)pathError; Assert.That(outOfRangeError.Offset, Is.EqualTo(new PointerOffset(1, false))); @@ -307,9 +274,48 @@ public void ReadIncompatibleTypeTest() Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnUnsupportedType))); } + #endregion + + #region Structure reading + + /// A struct with a couple fields, to test reading structs. + public record struct TestStruct(double A, int B); + /// Structure used to test reading a structure with references. private record struct TestStructWithReferences(int A, string B); + /// + /// Tests . + /// Reads a known struct from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. + /// + [Test] + public void ReadStructureTest() + { + var pointerPath = new PointerPath($"{OuterClassPointer:X}+30"); + Assert.That(TestProcessMemory!.Read(pointerPath).Value, + Is.EqualTo(new TestStruct(79879131651.33345, -7651))); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(pointerPath).Value, + Is.EqualTo(new TestStruct(-99879416311.4478, 987411))); + } + + /// + /// Tests . + /// Reads a known struct from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. + /// + [Test] + public void ReadStructureAsObjectTest() + { + var pointerPath = new PointerPath($"{OuterClassPointer:X}+30"); + Assert.That(TestProcessMemory!.Read(typeof(TestStruct), pointerPath).Value, + Is.EqualTo(new TestStruct(79879131651.33345, -7651))); + ProceedToNextStep(); + Assert.That(TestProcessMemory.Read(typeof(TestStruct), pointerPath).Value, + Is.EqualTo(new TestStruct(-99879416311.4478, 987411))); + } + /// /// Tests the method with a structure type that contains reference /// types. @@ -323,136 +329,342 @@ public void ReadStructureWithReferencesTest() Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnConversionFailure))); } + #endregion + + #region String reading + + #region FindStringSettings + + public record FindStringSettingsTestCase(Encoding Encoding, string String, bool IsNullTerminated, StringLengthPrefix? LengthPrefix, + byte[]? TypePrefix); + + private static readonly byte[] ExampleTypePrefix = { 0x11, 0x22, 0x33, 0x44 }; + + private static readonly FindStringSettingsTestCase[] FindStringSettingsTestCases = { + new(Encoding.Unicode, "Simple string", true, null, null), + new(Encoding.UTF8, "Simple string", true, null, null), + new(Encoding.UTF8, "String with diàcrîtìçs", true, null, null), + new(Encoding.Latin1, "String with diàcrîtìçs", true, null, null), + new(Encoding.Latin1, "é", true, null, null), + new(Encoding.Unicode, "Mµl十ÿहि鬱", true, null, null), + new(Encoding.Unicode, "Mµl十ÿहि鬱", true, new StringLengthPrefix(4, StringLengthPrefixUnit.Bytes), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(4, StringLengthPrefixUnit.Bytes), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(4, StringLengthPrefixUnit.Characters), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(2, StringLengthPrefixUnit.Characters), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(1, StringLengthPrefixUnit.Characters), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", false, new StringLengthPrefix(4, StringLengthPrefixUnit.Bytes), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", false, new StringLengthPrefix(4, StringLengthPrefixUnit.Characters), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(4, StringLengthPrefixUnit.Bytes), ExampleTypePrefix), + new(Encoding.UTF8, "Mµl十ÿहि鬱", true, null, ExampleTypePrefix), + }; + /// - /// Tests . - /// Reads a known string from the target process after initialization, without using any parameter beyond the - /// pointer path. - /// This method uses a to read the string at the right position. - /// It should be equal to its known values before and after modification. + /// Tests using predefined test cases. + /// The methodology is to allocate some memory, write a string to it using the parameters defined in the test case, + /// and then call the tested method on it. + /// The result is expected to match the test case settings. /// - [Test] - public void ReadStringWithNoParametersTest() + /// This test depends on allocation methods and writing methods. If all test cases fail, check the tests + /// for these features first. + [TestCaseSource(nameof(FindStringSettingsTestCases))] + public void FindStringSettingsTest(FindStringSettingsTestCase testCase) { - var pointerPath = new PointerPath($"{OuterClassPointer:X}+8,8"); - Assert.That(TestProcessMemory!.ReadString(pointerPath).GetValueOrDefault(), Is.EqualTo("ThisIsÄString")); - ProceedToNextStep(); - Assert.That(TestProcessMemory!.ReadString(pointerPath).GetValueOrDefault(), - Is.EqualTo("ThisIsALongerStrîngWith文字化けチェック")); + var settings = new StringSettings(testCase.Encoding) + { + IsNullTerminated = testCase.IsNullTerminated, + LengthPrefix = testCase.LengthPrefix, + TypePrefix = testCase.TypePrefix + }; + + // Allocate some memory to write the string. + var allocatedSpace = TestProcessMemory!.Allocate(1024, false).Value.ReserveRange(1024).Value; + byte[] bytes = settings.GetBytes(testCase.String) + ?? throw new ArgumentException("The test case is invalid: the length prefix is too short for the string."); + + // Fill the allocated space with FF bytes to prevent unexpected null termination results + TestProcessMemory.WriteBytes(allocatedSpace.Address, Enumerable.Repeat((byte)255, 1024).ToArray()); + + // Write the string to the allocated space. Because the FindStringSettings expects the address of a pointer + // to the string, we write the string with an offset of 8, and then we write the first 8 bytes to point to + // the address where we wrote the string. + TestProcessMemory.Write(allocatedSpace.Address + 8, bytes); + TestProcessMemory.Write(allocatedSpace.Address, allocatedSpace.Address + 8); + + // Call the tested method on the string pointer (the one we wrote last) + var findSettingsResult = TestProcessMemory.FindStringSettings(allocatedSpace.Address, testCase.String); + Assert.That(findSettingsResult.IsSuccess, Is.True); + + // Check that the settings match the test case, i.e. that the determined settings are the same settings we + // used to write the string in memory. + var foundSettings = findSettingsResult.Value; + Assert.Multiple(() => + { + Assert.That(foundSettings.Encoding, Is.EqualTo(testCase.Encoding)); + Assert.That(foundSettings.IsNullTerminated, Is.EqualTo(testCase.IsNullTerminated)); + Assert.That(foundSettings.LengthPrefix?.PrefixSize, Is.EqualTo(testCase.LengthPrefix?.PrefixSize)); + Assert.That(foundSettings.TypePrefix, Is.EqualTo(testCase.TypePrefix)); + }); } + + /// Settings expected for our target .net process. + private static readonly StringSettings ExpectedDotNetStringSettings = new(Encoding.Unicode, true, + new StringLengthPrefix(4, StringLengthPrefixUnit.Characters), new byte[8]); /// - /// Tests . - /// Reads a known string from the target process after initialization, with a max length of 10 bytes. - /// This method uses a to read the string at the right position. - /// It should be equal to the first 5 characters of its known value, because in memory, the string is stored as in - /// UTF-16, so it uses 2 bytes per ASCII-friendly character, so 10 bytes would be 5 characters. - /// Despite the length prefix being read correctly as more than 10, only the 10 first bytes should be read. + /// Tests on a known string in the target + /// process. /// [Test] - public void ReadStringWithLimitedLengthTest() - => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", 10).GetValueOrDefault(), - Is.EqualTo("ThisI")); + public void FindStringSettingsOnKnownPathTest() + { + var pointerPath = new PointerPath($"{OuterClassPointer:X}+8"); + var result = TestProcessMemory!.FindStringSettings(pointerPath, "ThisIsÄString"); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value.Encoding, Is.EqualTo(ExpectedDotNetStringSettings.Encoding)); + Assert.That(result.Value.IsNullTerminated, Is.EqualTo(ExpectedDotNetStringSettings.IsNullTerminated)); + Assert.That(result.Value.LengthPrefix?.PrefixSize, + Is.EqualTo(ExpectedDotNetStringSettings.LengthPrefix?.PrefixSize)); + Assert.That(result.Value.LengthPrefix?.Unit, Is.EqualTo(ExpectedDotNetStringSettings.LengthPrefix?.Unit)); + // For the type prefix, we only check the length, because the actual value is dynamic. + Assert.That(result.Value.TypePrefix?.Length, Is.EqualTo(ExpectedDotNetStringSettings.TypePrefix?.Length)); + } /// - /// Tests . - /// Reads a known string from the target process after initialization, with a StringSettings instance similar to the - /// .net preset but with no length prefix. - /// This method uses a to read the string at the right position, after its length prefix. - /// It should be equal to its full known value. Despite not being able to know the length of the string because we - /// specify that there is no length prefix, we still use a setting that specifies a null terminator, and the string - /// is indeed null-terminated, so it should properly cut after the last character. + /// Tests with a valid address but the wrong + /// expected string. + /// The method should return a . /// [Test] - public void ReadStringWithoutLengthPrefixTest() - => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,C", - stringSettings: new StringSettings(Encoding.Unicode, true, null)).GetValueOrDefault(), - Is.EqualTo("ThisIsÄString")); - + public void FindStringSettingsWithWrongStringTest() + { + var result = TestProcessMemory!.FindStringSettings(OuterClassPointer+8, "Wrong string"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnNoSettingsFound))); + } + /// - /// Tests . - /// Reads a known string from the target process after initialization, with a StringSettings instance similar to the - /// .net preset but not null-terminated. - /// This method uses a to read the string at the right position. - /// It should be equal to its full known value. Despite not being able to identify a null terminator as the end of - /// the string, we do use a setting that specifies a correct length prefix, so only the right number of bytes should - /// be read. + /// Tests with a valid address but an empty + /// expected string. + /// The method should return a . /// [Test] - public void ReadStringWithLengthPrefixWithoutNullTerminatorTest() - => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", - stringSettings: new StringSettings(Encoding.Unicode, false, new StringLengthPrefixSettings(4, 2))) - .GetValueOrDefault(), - Is.EqualTo("ThisIsÄString")); + public void FindStringSettingsWithEmptyStringTest() + { + var result = TestProcessMemory!.FindStringSettings(OuterClassPointer+8, ""); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnNoSettingsFound))); + } /// - /// Tests . - /// Reads a known string from the target process after initialization, with a StringSettings instance specifying no - /// null terminator and no length prefix. We use a byte count of 64. - /// This method uses a to read the string at the right position, after the length prefix. - /// It should be equal to its known value, followed by a bunch of garbage characters. Because we specified no length - /// prefix and no null terminator, all 64 characters will be read as a string, despite the actual string being - /// shorter than that. - /// To clarify, even though the result looks wrong, this is the expected output. The input parameters are wrong. + /// Tests on a zero pointer address. + /// The method should return a . /// [Test] - public void ReadStringWithoutLengthPrefixOrNullTerminatorTest() + public void FindStringSettingsOnZeroPointerTest() { - string? result = TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,C", 64, - new StringSettings(Encoding.Unicode, false, null)).GetValueOrDefault(); - - // We should have a string that starts with the full known string, and has at least one more character. - // We cannot test an exact string or length because the memory region after the string is not guaranteed to - // always be the same. - Assert.That(result, Does.StartWith("ThisIsÄString")); - Assert.That(result, Has.Length.AtLeast("ThisIsÄString".Length + 1)); + // We do not have a known zero pointer address, so we are going to allocate some memory and point to it. + var allocatedSpace = TestProcessMemory!.Allocate(8, false).Value.ReserveRange(8).Value; + var result = TestProcessMemory!.FindStringSettings(allocatedSpace.Address, "Whatever"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnZeroPointer))); } - + /// - /// Tests . - /// Reads a known string from the target process after initialization, with a StringSettings instance that has a - /// length prefix unit set to null instead of 2, and no null terminator. - /// This method uses a to read the string at the right position. - /// It should be equal to its full known value. The unit being set to null should trigger it to automatically - /// determine what the unit should be. It should determine that it is 2 and read the string correctly. + /// Tests with a pointer at the maximum possible + /// address, which is invalid. + /// The method should return a . /// [Test] - public void ReadStringWithUnspecifiedLengthPrefixUnitTest() - => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", - stringSettings: new StringSettings(Encoding.Unicode, false, new StringLengthPrefixSettings(4))) - .GetValueOrDefault(), - Is.EqualTo("ThisIsÄString")); + public void FindStringSettingsOnInvalidPointerAddressTest() + { + var result = TestProcessMemory!.FindStringSettings(UIntPtr.MaxValue, "Whatever"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnPointerReadFailure))); + var failure = (FindStringSettingsFailureOnPointerReadFailure)result.Error; + Assert.That(failure.Details, Is.TypeOf(typeof(ReadFailureOnSystemRead))); + } /// - /// Tests . - /// Reads a known string from the target process after initialization, with a StringSettings instance that has a - /// length prefix unit with a size of 2 instead of the correct 4. - /// This method uses a to read the string at the right position, 2 bytes into the length - /// prefix. - /// The result should be an empty string, because the length prefix should be read as 0. + /// Tests with a pointer to the maximum possible + /// address, which is invalid. + /// The method should return a . /// [Test] - public void ReadStringWithZeroLengthPrefixUnitTest() - => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,A", - stringSettings: new StringSettings(Encoding.Unicode, true, new StringLengthPrefixSettings(2, 2))) - .GetValueOrDefault(), - Is.EqualTo(string.Empty)); + public void FindStringSettingsOnInvalidStringAddressTest() + { + // Use a known path that should cause the string address to be 0xFFFFFFFFFFFFFFFF. + PointerPath pointerPath = $"{OuterClassPointer:X}+10,10"; + var result = TestProcessMemory!.FindStringSettings(pointerPath, "Whatever"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnStringReadFailure))); + var failure = (FindStringSettingsFailureOnStringReadFailure)result.Error; + Assert.That(failure.Details, Is.TypeOf(typeof(ReadFailureOnSystemRead))); + } /// - /// Tests . - /// Reads a known string from the target process after initialization, with a StringSettings instance that has a - /// UTF-8 encoding instead of the correct UTF-16. - /// This method uses a to read the string at the right position. - /// The result should be only the first character of the know value, because the null terminator is hit on the - /// second UTF-16 byte that is supposed to be part of the first character. - /// To explain a little bit more, a UTF-16 string has a minimum of 2 bytes per character. In this case, the first - /// two characters are "Th", which are held in memory as 54 00 68 00. But UTF-8 would write the same "Th" as 54 68. - /// The encoding will interpret the second byte (00) as a null terminator that signals the end of the string, and - /// read it only as "T" (54), discarding everything after that. + /// Tests with a pointer path that traverses + /// invalid addresses. + /// The method should return a . /// [Test] - public void ReadStringWithWrongEncodingTest() - => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", - stringSettings: new StringSettings(Encoding.UTF8, true, new StringLengthPrefixSettings(4, 2))) - .GetValueOrDefault(), - Is.EqualTo("T")); + public void FindStringSettingsOnInvalidPointerPathTest() + { + // Use a known pointer path that has a value of 0xFFFFFFFFFFFFFFFF, and add a ",1", which should cause a + // failure in the pointer path evaluation. + PointerPath pointerPath = $"{OuterClassPointer:X}+10,10,1"; + var result = TestProcessMemory!.FindStringSettings(pointerPath, "Whatever"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnPointerPathEvaluation))); + var failure = (FindStringSettingsFailureOnPointerPathEvaluation)result.Error; + Assert.That(failure.Details, Is.Not.Null); + } + + #endregion + + #region ReadRawString + + + + #endregion + + #region ReadStringPointer + + + + #endregion + + // /// + // /// Tests . + // /// Reads a known string from the target process after initialization, without using any parameter beyond the + // /// pointer path. + // /// This method uses a to read the string at the right position. + // /// It should be equal to its known values before and after modification. + // /// + // [Test] + // public void ReadStringWithNoParametersTest() + // { + // var pointerPath = new PointerPath($"{OuterClassPointer:X}+8,8"); + // Assert.That(TestProcessMemory!.ReadString(pointerPath).GetValueOrDefault(), Is.EqualTo("ThisIsÄString")); + // ProceedToNextStep(); + // Assert.That(TestProcessMemory!.ReadString(pointerPath).GetValueOrDefault(), + // Is.EqualTo("ThisIsALongerStrîngWith文字化けチェック")); + // } + // + // /// + // /// Tests . + // /// Reads a known string from the target process after initialization, with a max length of 10 bytes. + // /// This method uses a to read the string at the right position. + // /// It should be equal to the first 5 characters of its known value, because in memory, the string is stored as in + // /// UTF-16, so it uses 2 bytes per ASCII-friendly character, so 10 bytes would be 5 characters. + // /// Despite the length prefix being read correctly as more than 10, only the 10 first bytes should be read. + // /// + // [Test] + // public void ReadStringWithLimitedLengthTest() + // => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", 10).GetValueOrDefault(), + // Is.EqualTo("ThisI")); + // + // /// + // /// Tests . + // /// Reads a known string from the target process after initialization, with a StringSettings instance similar to the + // /// .net preset but with no length prefix. + // /// This method uses a to read the string at the right position, after its length prefix. + // /// It should be equal to its full known value. Despite not being able to know the length of the string because we + // /// specify that there is no length prefix, we still use a setting that specifies a null terminator, and the string + // /// is indeed null-terminated, so it should properly cut after the last character. + // /// + // [Test] + // public void ReadStringWithoutLengthPrefixTest() + // => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,C", + // stringSettings: new StringSettings(Encoding.Unicode, true, null)).GetValueOrDefault(), + // Is.EqualTo("ThisIsÄString")); + // + // /// + // /// Tests . + // /// Reads a known string from the target process after initialization, with a StringSettings instance similar to the + // /// .net preset but not null-terminated. + // /// This method uses a to read the string at the right position. + // /// It should be equal to its full known value. Despite not being able to identify a null terminator as the end of + // /// the string, we do use a setting that specifies a correct length prefix, so only the right number of bytes should + // /// be read. + // /// + // [Test] + // public void ReadStringWithLengthPrefixWithoutNullTerminatorTest() + // => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", + // stringSettings: new StringSettings(Encoding.Unicode, false, new StringLengthPrefix(4, 2))) + // .GetValueOrDefault(), + // Is.EqualTo("ThisIsÄString")); + // + // /// + // /// Tests . + // /// Reads a known string from the target process after initialization, with a StringSettings instance specifying no + // /// null terminator and no length prefix. We use a byte count of 64. + // /// This method uses a to read the string at the right position, after the length prefix. + // /// It should be equal to its known value, followed by a bunch of garbage characters. Because we specified no length + // /// prefix and no null terminator, all 64 characters will be read as a string, despite the actual string being + // /// shorter than that. + // /// To clarify, even though the result looks wrong, this is the expected output. The input parameters are wrong. + // /// + // [Test] + // public void ReadStringWithoutLengthPrefixOrNullTerminatorTest() + // { + // string? result = TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,C", 64, + // new StringSettings(Encoding.Unicode, false, null)).GetValueOrDefault(); + // + // // We should have a string that starts with the full known string, and has at least one more character. + // // We cannot test an exact string or length because the memory region after the string is not guaranteed to + // // always be the same. + // Assert.That(result, Does.StartWith("ThisIsÄString")); + // Assert.That(result, Has.Length.AtLeast("ThisIsÄString".Length + 1)); + // } + // + // /// + // /// Tests . + // /// Reads a known string from the target process after initialization, with a StringSettings instance that has a + // /// length prefix unit set to null instead of 2, and no null terminator. + // /// This method uses a to read the string at the right position. + // /// It should be equal to its full known value. The unit being set to null should trigger it to automatically + // /// determine what the unit should be. It should determine that it is 2 and read the string correctly. + // /// + // [Test] + // public void ReadStringWithUnspecifiedLengthPrefixUnitTest() + // => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", + // stringSettings: new StringSettings(Encoding.Unicode, false, new StringLengthPrefix(4))) + // .GetValueOrDefault(), + // Is.EqualTo("ThisIsÄString")); + // + // /// + // /// Tests . + // /// Reads a known string from the target process after initialization, with a StringSettings instance that has a + // /// length prefix unit with a size of 2 instead of the correct 4. + // /// This method uses a to read the string at the right position, 2 bytes into the length + // /// prefix. + // /// The result should be an empty string, because the length prefix should be read as 0. + // /// + // [Test] + // public void ReadStringWithZeroLengthPrefixUnitTest() + // => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,A", + // stringSettings: new StringSettings(Encoding.Unicode, true, new StringLengthPrefix(2, 2))) + // .GetValueOrDefault(), + // Is.EqualTo(string.Empty)); + // + // /// + // /// Tests . + // /// Reads a known string from the target process after initialization, with a StringSettings instance that has a + // /// UTF-8 encoding instead of the correct UTF-16. + // /// This method uses a to read the string at the right position. + // /// The result should be only the first character of the know value, because the null terminator is hit on the + // /// second UTF-16 byte that is supposed to be part of the first character. + // /// To explain a little bit more, a UTF-16 string has a minimum of 2 bytes per character. In this case, the first + // /// two characters are "Th", which are held in memory as 54 00 68 00. But UTF-8 would write the same "Th" as 54 68. + // /// The encoding will interpret the second byte (00) as a null terminator that signals the end of the string, and + // /// read it only as "T" (54), discarding everything after that. + // /// + // [Test] + // public void ReadStringWithWrongEncodingTest() + // => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", + // stringSettings: new StringSettings(Encoding.UTF8, true, new StringLengthPrefix(4, 2))) + // .GetValueOrDefault(), + // Is.EqualTo("T")); + + #endregion } \ No newline at end of file From f0393bd8f4c6f5ac3135a36ecafa95c580e69ed7 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Wed, 5 Jun 2024 17:43:47 +0200 Subject: [PATCH 14/66] Implemented ProcessMemoryStream, a stream to read/write into the memory of a process --- .../Addressing/ProcessMemoryStream.cs | 167 +++++++ .../Native/IOperatingSystemService.cs | 75 ++- .../Native/Win32Service.Imports.cs | 44 +- src/MindControl/Native/Win32Service.cs | 246 +++++++++- .../ProcessMemory/ProcessMemory.Addressing.cs | 27 ++ src/MindControl/Results/Result.cs | 11 +- .../ProcessMemoryStreamTest.cs | 439 ++++++++++++++++++ .../ProcessMemoryFindBytesTest.cs | 2 - .../ProcessMemoryTests/ProcessMemoryTest.cs | 2 + 9 files changed, 975 insertions(+), 38 deletions(-) create mode 100644 src/MindControl/Addressing/ProcessMemoryStream.cs create mode 100644 test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs diff --git a/src/MindControl/Addressing/ProcessMemoryStream.cs b/src/MindControl/Addressing/ProcessMemoryStream.cs new file mode 100644 index 0000000..8de40b9 --- /dev/null +++ b/src/MindControl/Addressing/ProcessMemoryStream.cs @@ -0,0 +1,167 @@ +using MindControl.Native; + +namespace MindControl; + +/// +/// A stream that reads or writes into the memory of a process. +/// +public class ProcessMemoryStream : Stream +{ + private readonly IOperatingSystemService _osService; + private readonly IntPtr _processHandle; + private readonly UIntPtr _baseAddress; + private UIntPtr _position; + + /// + /// Initializes a new instance of the class. + /// + /// Service that provides system-specific process memory read and write features. + /// Handle of the target process. + /// Starting address of the memory range to read or write. + internal ProcessMemoryStream(IOperatingSystemService osService, IntPtr processHandle, UIntPtr baseAddress) + { + _osService = osService; + _processHandle = processHandle; + _baseAddress = baseAddress; + _position = baseAddress; + } + + /// Returns True to indicate that this stream supports reading. + /// . + public override bool CanRead => true; + + /// Returns False to indicate that this stream does not support seeking. + /// . + public override bool CanSeek => false; + + /// Returns True to indicate that this stream supports writing. + /// + public override bool CanWrite => true; + + /// In this implementation, this getter is not supported. + /// Always thrown, as this implementation does not support + /// seeking. + /// A long value representing the length of the stream in bytes. + public override long Length => throw new NotSupportedException(); + + /// Gets or sets the position within the current stream, relative to the start address. + public override long Position + { + get => (long)(_position.ToUInt64() - _baseAddress.ToUInt64()); + set => _position = (UIntPtr)(_baseAddress.ToUInt64() + (ulong)value); + } + + /// In this implementation, does not perform any action. + public override void Flush() { } + + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the + /// number of bytes read. + /// An array of bytes. When this method returns, the buffer contains the specified byte array + /// with the values between and ( + - + /// 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in at which to begin storing the data + /// read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// The sum of and + /// is larger than the buffer length. + /// + /// is . + /// + /// or is negative. + /// An I/O error occurs. + /// Methods were called after the stream was closed. + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if + /// that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + public override int Read(byte[] buffer, int offset, int count) + { + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), "Offset cannot be negative."); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), "Count cannot be negative."); + if (buffer.Length - offset < count) + throw new ArgumentException("The buffer is too small to store the requested number of bytes.", + nameof(buffer)); + + var result = _osService.ReadProcessMemoryPartial(_processHandle, _position, buffer, offset, (ulong)count); + + // If no byte was read, return 0. + // We won't throw an exception here because multiple sorts of errors might mean that we cannot read any further + // and thus it's safer to consider it as the end of the stream. + if (result.IsFailure) + return 0; + + ulong read = result.Value; + _position = (UIntPtr)(_position.ToUInt64() + read); + return (int)read; + } + + /// Moves the position within the current stream by the specified offset, relative to either the start of + /// the stream (the base address), or the current position. Seeking relative to the end is not supported by this + /// implementation, as there is no end by design. + /// A byte offset relative to the parameter. + /// A value of type indicating the reference point used + /// to obtain the new position. In this implementation, is not supported. + /// Thrown when is set to + /// , as this implementation has no length and thus no end. + /// The new position within the current stream. + public override long Seek(long offset, SeekOrigin origin) + { + _position = origin switch + { + SeekOrigin.Begin => (UIntPtr)(_baseAddress.ToUInt64() + (ulong)offset), + SeekOrigin.Current => (UIntPtr)(_position.ToUInt64() + (ulong)offset), + SeekOrigin.End => throw new NotSupportedException("Seeking relative to the end is not supported."), + _ => throw new ArgumentOutOfRangeException(nameof(origin), "Invalid seek origin.") + }; + + return Position; + } + + /// In this implementation, this method is not supported. + /// The desired length of the current stream in bytes. + /// Always thrown, as this stream implementation does not support + /// seeking. + public override void SetLength(long value) => throw new NotSupportedException(); + + /// Writes a sequence of bytes to the current stream and advances the current position within this stream + /// by the number of bytes written. + /// An array of bytes. This method copies bytes from + /// to the current stream. + /// The zero-based byte offset in at which to begin copying bytes to + /// the current stream. + /// The number of bytes to be written to the current stream. + /// The sum of and + /// is greater than the buffer length. + /// + /// is . + /// + /// or is negative. + /// An I/O error occurred. + /// + /// was called after the stream was + /// closed. + public override void Write(byte[] buffer, int offset, int count) + { + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), "Offset cannot be negative."); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), "Count cannot be negative."); + if (buffer.Length - offset < count) + throw new ArgumentException("The buffer is too small to write the requested number of bytes.", + nameof(buffer)); + + var result = _osService.WriteProcessMemoryPartial(_processHandle, _position, buffer, offset, count); + + // Unlike Read, we will throw an exception if the write operation failed. + // This is because write operations are expected to fully complete. + if (result.IsFailure) + throw new IOException("Failed to write into the process memory.", result.ToException()); + + ulong written = result.Value; + if (written != (ulong)count) + throw new IOException($"Failed to write all bytes into the process memory. Only {written} bytes were written (expected {count})."); + + _position = (UIntPtr)(_position.ToUInt64() + written); + } +} + \ No newline at end of file diff --git a/src/MindControl/Native/IOperatingSystemService.cs b/src/MindControl/Native/IOperatingSystemService.cs index 3bd632d..096ce10 100644 --- a/src/MindControl/Native/IOperatingSystemService.cs +++ b/src/MindControl/Native/IOperatingSystemService.cs @@ -11,14 +11,14 @@ public interface IOperatingSystemService /// Opens the process with the given identifier, in a way that allows memory manipulation. /// /// Identifier of the target process. - /// Handle of the opened process. + /// A result holding either the handle of the opened process, or a system failure. Result OpenProcess(int pid); /// /// Returns a value indicating if the process with the given identifier is a 64-bit process or not. /// /// Identifier of the target process. - /// True if the process is 64-bits, false otherwise. + /// A result holding either a boolean indicating if the process is 64-bits, or a system failure. Result IsProcess64Bits(int pid); /// @@ -27,9 +27,26 @@ public interface IOperatingSystemService /// Handle of the target process. The handle must have PROCESS_VM_READ access. /// Starting address of the memory range to read. /// Length of the memory range to read. - /// An array of bytes containing the data read from the memory. + /// A result holding either an array of bytes containing the data read from the process memory, or a + /// system failure. Result ReadProcessMemory(IntPtr processHandle, UIntPtr baseAddress, ulong length); + /// + /// Reads a targeted range of the memory of a specified process into the given buffer. Supports partial reads, in + /// case the full length failed to be read but at least one byte was successfully copied into the buffer. + /// Prefer when you know the length of the data to read. + /// + /// Handle of the target process. The handle must have PROCESS_VM_READ access. + /// Starting address of the memory range to read. + /// Buffer to store the data read from the memory. The buffer must be large enough to store + /// the data read. + /// Offset in the buffer where the data will be stored. + /// Length of the memory range to read. + /// A result holding either the number of bytes actually read from memory, or a system failure when no byte + /// were successfully read. + Result ReadProcessMemoryPartial(IntPtr processHandle, UIntPtr baseAddress, byte[] buffer, + int offset, ulong length); + /// /// Overwrites the memory protection of the page that the given address is part of. /// Returns the memory protection that was effective on the page before being changed. @@ -39,11 +56,12 @@ public interface IOperatingSystemService /// A boolean indicating if the target process is 64 bits or not. /// An address in the target page. /// New protection value for the page. - /// The memory protection value that was effective on the page before being changed. + /// A result holding either the memory protection value that was effective on the page before being + /// changed, or a system failure. /// The process handle is invalid (zero pointer). /// The target address is invalid (zero pointer). - Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bits, UIntPtr targetAddress, - MemoryProtection newProtection); + Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bits, + UIntPtr targetAddress, MemoryProtection newProtection); /// /// Writes the given bytes into the memory of the specified process, at the target address. @@ -55,16 +73,36 @@ Result ReadAndOverwriteProtection(IntPtr proces /// written, unless a size is specified. /// Specify this value if you only want to write part of the value array in memory. /// This parameter is useful when using buffer byte arrays. Leave it to null to use the entire array. - Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, byte[] value, int? size = null); + /// A result indicating either a success or a system failure. + Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, byte[] value, + int? size = null); /// - /// Allocates memory in the specified process. + /// Writes the given bytes into the memory of the specified process, at the target address. Supports partial reads, + /// in case the full length failed to be written but at least one byte was successfully written. + /// Prefer in most cases. + /// + /// Handle of the target process. The handle must have PROCESS_VM_WRITE and + /// PROCESS_VM_OPERATION access. + /// Base address in the memory of the process to which data will be written. + /// Byte array to write in the memory. Depending on the and + /// parameters, only part of the buffer may be copied into the process memory. + /// Offset in the buffer where the data to write starts. + /// Number of bytes to write from the buffer into the process memory, starting from the + /// . + /// A result holding either the number of bytes written, or a system failure when no bytes were written. + /// + Result WriteProcessMemoryPartial(IntPtr processHandle, UIntPtr targetAddress, byte[] buffer, + int offset, int size); + + /// + /// Allocates memory in the specified process. The address is determined automatically by the operating system. /// /// Handle of the target process. /// Size in bytes of the memory to allocate. /// Type of memory allocation. /// Protection flags of the memory to allocate. - /// A pointer to the start of the allocated memory. + /// A result holding either a pointer to the start of the allocated memory, or a system failure. Result AllocateMemory(IntPtr processHandle, int size, MemoryAllocationType allocationType, MemoryProtection protection); @@ -76,13 +114,14 @@ Result AllocateMemory(IntPtr processHandle, int size, Me /// Size in bytes of the memory to allocate. /// Type of memory allocation. /// Protection flags of the memory to allocate. - /// A pointer to the start of the allocated memory. - Result AllocateMemory(IntPtr processHandle, UIntPtr address, int size, MemoryAllocationType allocationType, - MemoryProtection protection); + /// A result holding either a pointer to the start of the allocated memory, or a system failure. + Result AllocateMemory(IntPtr processHandle, UIntPtr address, int size, + MemoryAllocationType allocationType, MemoryProtection protection); /// /// Gets the address of the function used to load a library in the current process. /// + /// A result holding either the address of the function, or a system failure. Result GetLoadLibraryFunctionAddress(); /// @@ -91,16 +130,17 @@ Result AllocateMemory(IntPtr processHandle, UIntPtr addr /// Handle of the target process. /// Address of the start routine to be executed by the thread. /// Address of any parameter to be passed to the start routine. - /// Handle of the thread. - Result CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, UIntPtr parameterAddress); + /// A result holding either the handle of the thread, or a system failure. + Result CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, + UIntPtr parameterAddress); /// /// Waits for the specified thread to finish execution. /// /// Handle of the target thread. /// Maximum time to wait for the thread to finish. - /// True if the thread finished execution, false if the timeout was reached. Other failures will return a - /// failure. + /// A result holding either a boolean indicating if the thread returned (True) or timed out (False), or a + /// system failure for other error cases. Result WaitThread(IntPtr threadHandle, TimeSpan timeout); /// @@ -109,12 +149,14 @@ Result AllocateMemory(IntPtr processHandle, UIntPtr addr /// Handle of the target process. /// Base address of the region or placeholder to free, as returned by the allocation /// methods. + /// A result indicating either a success or a system failure. Result ReleaseMemory(IntPtr processHandle, UIntPtr regionBaseAddress); /// /// Closes the given handle. /// /// Handle to close. + /// A result indicating either a success or a system failure. Result CloseHandle(IntPtr handle); /// @@ -129,6 +171,7 @@ Result AllocateMemory(IntPtr processHandle, UIntPtr addr /// Base address of the target memory region. /// A boolean indicating if the target process is 64 bits or not. /// If left null, the method will automatically determine the bitness of the process. + /// A result holding either the metadata of the target memory region, or a system failure. Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress, bool is64Bits); diff --git a/src/MindControl/Native/Win32Service.Imports.cs b/src/MindControl/Native/Win32Service.Imports.cs index 052ae84..df61801 100644 --- a/src/MindControl/Native/Win32Service.Imports.cs +++ b/src/MindControl/Native/Win32Service.Imports.cs @@ -408,7 +408,27 @@ private static class WaitForSingleObjectResult /// If the function fails, the return value is 0. [DllImport("kernel32.dll", SetLastError = true)] private static extern int ReadProcessMemory(IntPtr hProcess, UIntPtr lpBaseAddress, [Out] byte[] lpBuffer, - ulong nSize, out ulong lpNumberOfBytesRead); + ulong nSize, out UIntPtr lpNumberOfBytesRead); + + /// + /// Reads memory in the given process. This variant uses a pointer for the buffer, which allows callers to + /// avoid unnecessary memory allocations in certain cases. + /// + /// A handle to the process with memory that is being read. The handle must have + /// PROCESS_VM_READ access to the process. + /// A pointer to the base address in the specified process from which to read. Before + /// any data transfer occurs, the system verifies that all data in the base address and memory of the specified size + /// is accessible for read access, and if it is not accessible the function fails. + /// A pointer to a buffer that receives the contents from the address space of the specified + /// process. + /// The number of bytes to be read from the specified process. + /// A pointer to a variable that receives the number of bytes transferred into the + /// specified buffer. + /// If the function succeeds, the return value is nonzero. + /// If the function fails, the return value is 0. + [DllImport("kernel32.dll", SetLastError = true)] + private static extern int ReadProcessMemory(IntPtr hProcess, UIntPtr lpBaseAddress, UIntPtr lpBuffer, + ulong nSize, out UIntPtr lpNumberOfBytesRead); /// /// Changes the protection on a region of committed pages in the virtual address space of a specified process. @@ -444,5 +464,25 @@ public static extern bool VirtualProtectEx(IntPtr hProcess, UIntPtr lpAddress, /// If the function succeeds, the return value is true. Otherwise, it will be false. [DllImport("kernel32.dll", SetLastError = true)] public static extern bool WriteProcessMemory(IntPtr hProcess, UIntPtr lpBaseAddress, byte[] lpBuffer, UIntPtr nSize, - IntPtr lpNumberOfBytesWritten); + out UIntPtr lpNumberOfBytesWritten); + + /// + /// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible + /// or the operation fails. This variant uses a pointer for the buffer, which allows callers to avoid unnecessary + /// memory allocations in certain cases. + /// + /// A handle to the process memory to be modified. The handle must have PROCESS_VM_WRITE and + /// PROCESS_VM_OPERATION access to the process. + /// A pointer to the base address in the specified process to which data is written. + /// Before data transfer occurs, the system verifies that all data in the base address and memory of the specified + /// size is accessible for write access, and if it is not accessible, the function fails. + /// A pointer to the buffer that contains data to be written in the address space of the + /// specified process. + /// The number of bytes to be written to the specified process. + /// A pointer to a variable that receives the number of bytes transferred into + /// the specified process. This parameter is optional. If null, it will be ignored. + /// If the function succeeds, the return value is true. Otherwise, it will be false. + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool WriteProcessMemory(IntPtr hProcess, UIntPtr lpBaseAddress, UIntPtr lpBuffer, + UIntPtr nSize, out UIntPtr lpNumberOfBytesWritten); } \ No newline at end of file diff --git a/src/MindControl/Native/Win32Service.cs b/src/MindControl/Native/Win32Service.cs index dee90e1..66629dc 100644 --- a/src/MindControl/Native/Win32Service.cs +++ b/src/MindControl/Native/Win32Service.cs @@ -25,7 +25,7 @@ private static OperatingSystemCallFailure GetLastSystemError() /// Opens the process with the given identifier, in a way that allows memory manipulation. /// /// Identifier of the target process. - /// Handle of the opened process. + /// A result holding either the handle of the opened process, or a system failure. public Result OpenProcess(int pid) { var handle = OpenProcess(0x1F0FFF, true, pid); @@ -39,7 +39,7 @@ public Result OpenProcess(int pid) /// Returns a value indicating if the process with the given identifier is a 64-bit process or not. /// /// Identifier of the target process. - /// True if the process is 64-bits, false otherwise. + /// A result holding either a boolean indicating if the process is 64-bits, or a system failure. public Result IsProcess64Bits(int pid) { try @@ -48,10 +48,8 @@ public Result IsProcess64Bits(int pid) if (!IsWow64Process(process.Handle, out bool isWow64)) return GetLastSystemError(); - bool isSystem64Bits = IntPtr.Size == 8; - // Process is 64 bits if we are running a 64-bits system and the process is NOT in wow64. - return !isWow64 && isSystem64Bits; + return !isWow64 && IsSystem64Bits(); } catch (Exception) { @@ -60,13 +58,20 @@ public Result IsProcess64Bits(int pid) } } + /// + /// Returns a value indicating if the system is 64 bits or not. + /// + /// A boolean indicating if the system is 64 bits or not. + private bool IsSystem64Bits() => IntPtr.Size == 8; + /// /// Reads a targeted range of the memory of a specified process. /// /// Handle of the target process. The handle must have PROCESS_VM_READ access. /// Starting address of the memory range to read. /// Length of the memory range to read. - /// An array of bytes containing the data read from the memory. + /// A result holding either an array of bytes containing the data read from the process memory, or a + /// system failure. public Result ReadProcessMemory(IntPtr processHandle, UIntPtr baseAddress, ulong length) { if (processHandle == IntPtr.Zero) @@ -78,6 +83,144 @@ public Result ReadProcessMemory(IntPtr processHandle, UIn return returnValue == 0 ? GetLastSystemError() : result; } + + /// + /// Reads a targeted range of the memory of a specified process into the given buffer. Supports partial reads, in + /// case the full length failed to be read but at least one byte was successfully copied into the buffer. + /// Prefer when you know the length of the data to read. + /// + /// Handle of the target process. The handle must have PROCESS_VM_READ access. + /// Starting address of the memory range to read. + /// Buffer to store the data read from the memory. The buffer must be large enough to store + /// the data read. + /// Offset in the buffer where the data will be stored. + /// Length of the memory range to read. + /// A result holding either the number of bytes actually read from memory, or a system failure. + public Result ReadProcessMemoryPartial(IntPtr processHandle, UIntPtr baseAddress, + byte[] buffer, int offset, ulong length) + { + if (processHandle == IntPtr.Zero) + return new SystemFailureOnInvalidArgument(nameof(processHandle), + "The process handle is invalid (zero pointer)."); + if ((ulong)buffer.Length < (ulong)offset + length) + return new SystemFailureOnInvalidArgument(nameof(buffer), + "The buffer is too small to store the requested number of bytes."); + if (UIntPtr.MaxValue.ToUInt64() - baseAddress.ToUInt64() < length) + return new SystemFailureOnInvalidArgument(nameof(length), + "The base address plus the length to read exceeds the maximum possible address."); + + // We need to take in account the offset, meaning we can only write to the buffer from a certain position, + // defined as the "offset" parameter. + // This is a problem, because the Win32 API doesn't have that. It starts writing from the beginning of whatever + // buffer you pass in. + // But in fact, as with any array, the Win32 API sees the buffer as a pointer. This means we can use an + // alternative signature that uses a UIntPtr as the buffer instead of a byte array. + // Which, in turns, means that we can call it with the address of a specific element in the buffer, and the API + // will start writing from there. + + // To get the pointer to the right element in the buffer, we first have to pin the buffer in memory. + // This ensures that the garbage collector doesn't move the buffer around while we're working with it. + var bufferGcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + // Then we use a Marshal method to get a pointer to the element in the buffer at the given offset. + var bufferPtr = (UIntPtr)Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset).ToInt64(); + + // Finally, we can call the Win32 API with the pointer to the right element in the buffer. + int returnValue = ReadProcessMemory(processHandle, baseAddress, bufferPtr, length, out var bytesRead); + + // If the function is a success or read at least one byte, we return the number of bytes read. + if (bytesRead != UIntPtr.Zero || returnValue != 0) + return bytesRead.ToUInt64(); + var initialReadError = GetLastSystemError(); + + // If we are here, we know that the function failed, and also didn't read anything. + // This may mean that the whole range is unreadable, but might also mean that only part of it is. + // Sadly, the Win32 API will fail in this way even if only one byte is unreadable. + // The strategy now is going to be to query for memory regions in the range, and determine at which point + // the memory that we want stops being readable. We then read up to that point. + // We do that extended procedure only if the first read failed. This should make most cases faster. + + // Edge case: if we attempted to read only one byte, we won't be able to reduce the range to read, so we + // return immediately with the first reading error. + if (length == 1) + return initialReadError; + + // Determine if the process is 64 bits + if (!IsWow64Process(processHandle, out bool isWow64)) + return GetLastSystemError(); + bool is64Bits = !isWow64 && IsSystem64Bits(); + + // Build the memory range that spans across everything we attempted to read + var range = MemoryRange.FromStartAndSize(baseAddress, length); + + // Determine the last readable address within the range + var lastReadableAddress = GetLastConsecutiveReadableAddressWithinRange(processHandle, is64Bits, range); + + // If we couldn't determine the last readable address, or it matches/exceeds the end of the range, there + // is no point in trying to read again. Return the initial read error. + if (lastReadableAddress == null || lastReadableAddress.Value.ToUInt64() >= range.End.ToUInt64()) + return initialReadError; + + // If we found a readable address within the range, we read up to that point. + var newLength = lastReadableAddress.Value.ToUInt64() - baseAddress.ToUInt64(); + returnValue = ReadProcessMemory(processHandle, baseAddress, bufferPtr, newLength, out bytesRead); + + // If the function failed again and didn't read any byte again, return the read error. + if (bytesRead == UIntPtr.Zero && returnValue == 0) + return GetLastSystemError(); + + // In other cases, we return the number of bytes read. + return bytesRead.ToUInt64(); + } + finally + { + // After using the pinned buffer, we must free it, so that the garbage collector can handle it again. + bufferGcHandle.Free(); + } + } + + /// + /// Given a range, checks memory regions within that range starting from the start of the range upwards, and returns + /// the end address of the last consecutive readable region. If the start of the range is unreadable, returns null. + /// + /// Handle of the target process. + /// A boolean indicating if the target process is 64 bits or not. + /// Memory range to check for readable regions. + /// The end address of the last consecutive readable region within the range, or null if the start of the + /// range is unreadable. + private UIntPtr? GetLastConsecutiveReadableAddressWithinRange(IntPtr processHandle, bool is64Bits, + MemoryRange range) + { + var applicationMemoryLimit = GetFullMemoryRange().End; + ulong rangeEnd = Math.Min(range.End.ToUInt64(), applicationMemoryLimit.ToUInt64()); + UIntPtr currentAddress = range.Start; + while (currentAddress.ToUInt64() <= rangeEnd) + { + var getRegionResult = GetRegionMetadata(processHandle, currentAddress, is64Bits); + + // If we failed to get the region metadata, stop iterating. + if (getRegionResult.IsFailure) + break; + + var currentRangeMetadata = getRegionResult.Value; + + // If the current region is not readable, stop iterating. + if (!currentRangeMetadata.IsReadable) + break; + + // Keep iterating to the next region. + currentAddress = (UIntPtr)(currentRangeMetadata.StartAddress.ToUInt64() + + currentRangeMetadata.Size.ToUInt64()); + } + + // If the current address is still the same as the start of the range, it means the start of the range is + // unreadable. + if (currentAddress == range.Start) + return null; + + return currentAddress - 1; + } /// /// Overwrites the memory protection of the page that the given address is part of. @@ -88,7 +231,8 @@ public Result ReadProcessMemory(IntPtr processHandle, UIn /// A boolean indicating if the target process is 64 bits or not. /// An address in the target page. /// New protection value for the page. - /// The memory protection value that was effective on the page before being changed. + /// A result holding either the memory protection value that was effective on the page before being + /// changed, or a system failure. public Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bits, UIntPtr targetAddress, MemoryProtection newProtection) { @@ -99,7 +243,7 @@ public Result ReadAndOverwriteProtection(IntPtr return new SystemFailureOnInvalidArgument(nameof(targetAddress), "The target address cannot be a zero pointer."); - bool result = VirtualProtectEx(processHandle, targetAddress, (IntPtr)(is64Bits ? 8 : 4), newProtection, + var result = VirtualProtectEx(processHandle, targetAddress, (IntPtr)(is64Bits ? 8 : 4), newProtection, out var previousProtection); return result ? previousProtection : GetLastSystemError(); @@ -115,6 +259,7 @@ public Result ReadAndOverwriteProtection(IntPtr /// written, unless a size is specified. /// Specify this value if you only want to write part of the value array in memory. /// This parameter is useful when using buffer byte arrays. Leave it to null to use the entire array. + /// A result indicating either a success or a system failure. public Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, byte[] value, int? size = null) { if (processHandle == IntPtr.Zero) @@ -127,12 +272,72 @@ public Result WriteProcessMemory(IntPtr processHandle, UIntPtr ta return new SystemFailureOnInvalidArgument(nameof(size), "The size cannot exceed the length of the value array."); - bool result = WriteProcessMemory(processHandle, targetAddress, value, (UIntPtr)(size ?? value.Length), - IntPtr.Zero); + var result = WriteProcessMemory(processHandle, targetAddress, value, (UIntPtr)(size ?? value.Length), + out _); return result ? Result.Success : GetLastSystemError(); } + /// + /// Writes the given bytes into the memory of the specified process, at the target address. Supports partial reads, + /// in case the full length failed to be written but at least one byte was successfully written. + /// Prefer in most cases. + /// + /// Handle of the target process. The handle must have PROCESS_VM_WRITE and + /// PROCESS_VM_OPERATION access. + /// Base address in the memory of the process to which data will be written. + /// Byte array to write in the memory. Depending on the and + /// parameters, only part of the buffer may be copied into the process memory. + /// Offset in the buffer where the data to write starts. + /// Number of bytes to write from the buffer into the process memory, starting from the + /// . + /// A result holding either the number of bytes written, or a system failure when no bytes were written. + /// + public Result WriteProcessMemoryPartial(IntPtr processHandle, UIntPtr targetAddress, + byte[] buffer, int offset, int size) + { + if (processHandle == IntPtr.Zero) + return new SystemFailureOnInvalidArgument(nameof(processHandle), + "The process handle is invalid (zero pointer)."); + if (buffer.Length < offset + size) + return new SystemFailureOnInvalidArgument(nameof(buffer), + "The buffer is too small to write the requested number of bytes."); + + // We need to take in account the offset, meaning we can only copy from the buffer from a certain position, + // defined as the "offset" parameter. + // This is a problem, because the Win32 API doesn't have that. It starts copying from the beginning of whatever + // buffer you pass in. + // But in fact, as with any array, the Win32 API sees the buffer as a pointer. This means we can use an + // alternative signature that uses a UIntPtr as the buffer instead of a byte array. + // Which, in turns, means that we can call it with the address of a specific element in the buffer, and the API + // will start copying from there. + + // To get the pointer to the right element in the buffer, we first have to pin the buffer in memory. + // This ensures that the garbage collector doesn't move the buffer around while we're working with it. + var bufferGcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + // Then we use a Marshal method to get a pointer to the element in the buffer at the given offset. + var bufferPtr = (UIntPtr)Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset).ToInt64(); + + // Finally, we can call the Win32 API with the pointer to the right element in the buffer. + bool returnValue = WriteProcessMemory(processHandle, targetAddress, bufferPtr, (UIntPtr)size, + out var bytesWritten); + + // Only return a failure when the function failed AND didn't write anything. + // This ensures that a non-error result is returned for a partial write. + if (bytesWritten == UIntPtr.Zero && !returnValue) + return GetLastSystemError(); + + return bytesWritten.ToUInt64(); + } + finally + { + // After using the pinned buffer, we must free it, so that the garbage collector can handle it again. + bufferGcHandle.Free(); + } + } + /// /// Allocates memory in the specified process. /// @@ -140,7 +345,7 @@ public Result WriteProcessMemory(IntPtr processHandle, UIntPtr ta /// Size in bytes of the memory to allocate. /// Type of memory allocation. /// Protection flags of the memory to allocate. - /// A pointer to the start of the allocated memory. + /// A result holding either a pointer to the start of the allocated memory, or a system failure. public Result AllocateMemory(IntPtr processHandle, int size, MemoryAllocationType allocationType, MemoryProtection protection) => AllocateMemory(processHandle, UIntPtr.Zero, size, allocationType, protection); @@ -153,7 +358,7 @@ public Result AllocateMemory(IntPtr processHandle, int s /// Size in bytes of the memory to allocate. /// Type of memory allocation. /// Protection flags of the memory to allocate. - /// A pointer to the start of the allocated memory. + /// A result holding either a pointer to the start of the allocated memory, or a system failure. public Result AllocateMemory(IntPtr processHandle, UIntPtr address, int size, MemoryAllocationType allocationType, MemoryProtection protection) { @@ -172,7 +377,7 @@ public Result AllocateMemory(IntPtr processHandle, UIntP /// /// Name or path of the module. This module must be loaded in the current process. /// Name of the target function in the specified module. - /// The address of the function if located, or null otherwise. + /// A result holding the address of the function if located, or a system failure otherwise. private Result GetFunctionAddress(string moduleName, string functionName) { var moduleHandle = GetModuleHandle(moduleName); @@ -189,6 +394,7 @@ private Result GetFunctionAddress(string moduleName, str /// /// Gets the address of the function used to load a library in the current process. /// + /// A result holding either the address of the function, or a system failure. public Result GetLoadLibraryFunctionAddress() => GetFunctionAddress("kernel32.dll", "LoadLibraryW"); @@ -198,7 +404,7 @@ public Result GetLoadLibraryFunctionAddress() /// Handle of the target process. /// Address of the start routine to be executed by the thread. /// Address of any parameter to be passed to the start routine. - /// Handle of the thread. + /// A result holding either the handle of the thread, or a system failure. public Result CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, UIntPtr parameterAddress) { @@ -221,8 +427,8 @@ public Result CreateRemoteThread(IntPtr processHandle, UI /// /// Handle of the target thread. /// Maximum time to wait for the thread to finish. - /// True if the thread finished execution, false if the timeout was reached. Other failures will return - /// a failure. + /// A result holding either a boolean indicating if the thread returned (True) or timed out (False), or a + /// system failure for other error cases. public Result WaitThread(IntPtr threadHandle, TimeSpan timeout) { if (threadHandle == IntPtr.Zero) @@ -244,6 +450,7 @@ public Result WaitThread(IntPtr threadHandle, TimeSpan time /// Handle of the target process. /// Base address of the region or placeholder to free, as returned by the memory /// allocation methods. + /// A result indicating either a success or a system failure. public Result ReleaseMemory(IntPtr processHandle, UIntPtr regionBaseAddress) { if (processHandle == IntPtr.Zero) @@ -261,6 +468,7 @@ public Result ReleaseMemory(IntPtr processHandle, UIntPtr regionB /// Closes the given handle. /// /// Handle to close. + /// A result indicating either a success or a system failure. public Result CloseHandle(IntPtr handle) { if (handle == IntPtr.Zero) @@ -308,6 +516,7 @@ public MemoryRange GetFullMemoryRange() /// Handle of the target process. /// Base address of the target memory region. /// A boolean indicating if the target process is 64 bits or not. + /// A result holding either the metadata of the target memory region, or a system failure. public Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress, bool is64Bits) { @@ -347,7 +556,10 @@ public Result GetRegionMetadata(IntPtr proce IsProtected = memoryBasicInformation.Protect.HasFlag(MemoryProtection.PageGuard) || memoryBasicInformation.Protect.HasFlag(MemoryProtection.NoAccess), IsMapped = memoryBasicInformation.Type == PageType.Mapped, - IsReadable = memoryBasicInformation.Protect.HasFlag(MemoryProtection.ReadOnly), + IsReadable = memoryBasicInformation.Protect.HasFlag(MemoryProtection.ReadOnly) + || memoryBasicInformation.Protect.HasFlag(MemoryProtection.ReadWrite) + || memoryBasicInformation.Protect.HasFlag(MemoryProtection.ExecuteRead) + || memoryBasicInformation.Protect.HasFlag(MemoryProtection.ExecuteReadWrite), IsWritable = memoryBasicInformation.Protect.HasFlag(MemoryProtection.ReadWrite) || memoryBasicInformation.Protect.HasFlag(MemoryProtection.WriteCopy) || memoryBasicInformation.Protect.HasFlag(MemoryProtection.ExecuteReadWrite) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs index b06f788..c9a71d3 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs @@ -74,6 +74,33 @@ public Result EvaluateMemoryAddress(PointerPath return currentAddress; } + + /// + /// Creates and returns a new instance that starts at the address pointed by the + /// given path. + /// The stream can be used to read or write into the process memory. It is owned by the caller and must be disposed + /// when no longer needed. + /// + /// Pointer path to the starting address of the stream. + /// A result holding either the created process memory stream, or a path evaluation failure. + public Result GetMemoryStream(PointerPath pointerPath) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + if (addressResult.IsFailure) + return addressResult.Error; + + return GetMemoryStream(addressResult.Value); + } + + /// + /// Creates and returns a new instance that starts at the given address. + /// The stream can be used to read or write into the process memory. It is owned by the caller and must be disposed + /// when no longer needed. + /// + /// Starting address of the stream. + /// The created process memory stream. + public ProcessMemoryStream GetMemoryStream(UIntPtr startAddress) + => new ProcessMemoryStream(_osService, _processHandle, startAddress); /// /// Gets the process module with the given name. diff --git a/src/MindControl/Results/Result.cs b/src/MindControl/Results/Result.cs index a755fa5..77136c4 100644 --- a/src/MindControl/Results/Result.cs +++ b/src/MindControl/Results/Result.cs @@ -58,9 +58,18 @@ protected Result(TError error) public void ThrowOnError() { if (IsFailure) - throw new ResultFailureException(Error); + throw ToException(); } + /// + /// Converts the result to an exception if it represents a failure. + /// + /// A new instance if the operation was a failure. + /// Thrown if the operation was successful and thus cannot be + /// converted to an exception. + public ResultFailureException ToException() => IsFailure ? new ResultFailureException(Error) + : throw new InvalidOperationException("Cannot convert a successful result to an exception."); + /// /// Creates a new failed instance. /// diff --git a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs new file mode 100644 index 0000000..9ee1284 --- /dev/null +++ b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs @@ -0,0 +1,439 @@ +using System.Runtime.InteropServices; +using MindControl.Test.ProcessMemoryTests; +using NUnit.Framework; + +namespace MindControl.Test.AddressingTests; + +/// +/// Tests the class. +/// Because this class is strongly bound to a ProcessMemory, we have to use the +/// method to create instances, so the tests below will use an +/// actual instance of and depend on that method. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryStreamTest : ProcessMemoryTest +{ + /// + /// Tests the method. + /// This is not part of the tested class, but it is used to create instances of it, so that's why it's here. + /// + [Test] + public void GetMemoryStreamTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(stream, Is.Not.Null); + } + + #region Getters and setters + + /// + /// Tests the getter. + /// Must always return true. + /// + [Test] + public void CanReadTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(stream.CanRead, Is.True); + } + + /// + /// Tests the getter. + /// Must always return false. + /// + [Test] + public void CanSeekTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(stream.CanSeek, Is.False); + } + + /// + /// Tests the getter. + /// Must always return true. + /// + [Test] + public void CanWriteTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(stream.CanWrite, Is.True); + } + + /// + /// Tests the getter. + /// Must always throw, as the stream has no length by design. + /// + [Test] + public void LengthTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(() => stream.Length, Throws.InstanceOf()); + } + + /// + /// Tests the method. + /// Must always throw, as the stream has no length by design. + /// + [Test] + public void SetLengthTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(() => stream.SetLength(0), Throws.InstanceOf()); + Assert.That(() => stream.SetLength(32), Throws.InstanceOf()); + Assert.That(() => stream.SetLength(256), Throws.InstanceOf()); + } + + /// + /// Tests the getter. + /// Must return 0 on a new instance, and advance as data is read or written. + /// + [Test] + public void PositionTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(stream.Position, Is.Zero); + + stream.ReadByte(); + Assert.That(stream.Position, Is.EqualTo(1)); + + stream.WriteByte(0x00); + Assert.That(stream.Position, Is.EqualTo(2)); + + stream.Position = 0; + Assert.That(stream.Position, Is.Zero); + } + + #endregion + + #region Flush + + /// + /// Tests the method. + /// Must not throw, but is expected to do nothing, as the stream is not buffered. + /// + [Test] + public void FlushTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(() => stream.Flush(), Throws.Nothing); + } + + #endregion + + #region Seek + + /// + /// Tests the method. + /// Tests that seeking back and forth with changes the position as expected. + /// + [Test] + public void SeekFromBeginTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + stream.Seek(8, SeekOrigin.Begin); + Assert.That(stream.Position, Is.EqualTo(8)); + + stream.Seek(0, SeekOrigin.Begin); + Assert.That(stream.Position, Is.Zero); + } + + /// + /// Tests the method. + /// Tests that seeking with changes the position as expected. + /// + [Test] + public void SeekFromCurrentTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + stream.Seek(8, SeekOrigin.Current); + Assert.That(stream.Position, Is.EqualTo(8)); + + stream.Seek(0, SeekOrigin.Current); + Assert.That(stream.Position, Is.EqualTo(8)); + } + + /// + /// Tests the method. + /// Tests that seeking with throws a , as there is + /// no end of the stream, by design. + /// + [Test] + public void SeekFromEndTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(() => stream.Seek(8, SeekOrigin.End), Throws.InstanceOf()); + } + + #endregion + + #region Read + + /// + /// Tests the method. + /// Performs a single read operation over a known value. + /// Must read the expected data from the process memory. + /// + [Test] + public void SimpleReadTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer + 0x28); + var buffer = new byte[8]; + int byteCount = stream.Read(buffer, 0, 8); + ulong readValue = MemoryMarshal.Read(buffer); + + Assert.That(byteCount, Is.EqualTo(8)); + Assert.That(readValue, Is.EqualTo(76354111324644L)); // Known value + } + + /// + /// Tests the method. + /// Performs two read operations over known values. + /// Must read the expected data from the process memory. + /// This tests that the read methods read from the current position in the stream, and not always from the start. + /// + [Test] + public void MultipleReadTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer + 0x20); + var buffer = new byte[8]; + int firstByteCount = stream.Read(buffer, 0, 8); + long firstValue = MemoryMarshal.Read(buffer); + + int secondByteCount = stream.Read(buffer, 0, 8); + ulong secondValue = MemoryMarshal.Read(buffer); + + Assert.That(firstByteCount, Is.EqualTo(8)); + Assert.That(firstValue, Is.EqualTo(-65746876815103L)); // Known value + + Assert.That(secondByteCount, Is.EqualTo(8)); + Assert.That(secondValue, Is.EqualTo(76354111324644L)); // Known value + } + + /// + /// Tests the method. + /// Performs a single read operation over a known value, with an offset. + /// Must read the expected data from the process memory, with the offset applied. + /// + [Test] + public void ReadWithOffsetTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer + 0x28); + var buffer = new byte[12]; + int byteCount = stream.Read(buffer, 4, 8); // Use an offset of 4 + ulong readValue = MemoryMarshal.Read(buffer.AsSpan(4)); // Read value from index 4 + + Assert.That(byteCount, Is.EqualTo(8)); + Assert.That(buffer.Take(4), Is.All.Zero); // First 4 bytes are untouched + Assert.That(readValue, Is.EqualTo(76354111324644L)); // Known value + } + + /// + /// Tests the method. + /// Calls the read method with an offset that goes beyond the capacity of the buffer. + /// Must throw a . + /// + [Test] + public void ReadWithImpossibleOffsetTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(() => stream.Read(new byte[8], 8, 1), Throws.InstanceOf()); + } + + /// + /// Tests the method. + /// Calls the read method with a count that goes beyond the capacity of the buffer. + /// Must throw a . + /// + [Test] + public void ReadWithImpossibleCountTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(() => stream.Read(new byte[8], 0, 9), Throws.InstanceOf()); + } + + /// + /// Tests the method. + /// Calls the read method with a combination of offset and count that go beyond the capacity of the buffer. + /// Must throw a . + /// + [Test] + public void ReadWithImpossibleOffsetAndCountTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(() => stream.Read(new byte[8], 3, 6), Throws.InstanceOf()); + } + + /// + /// Tests the method. + /// Performs a read operation that starts in a readable region, but ends in a non-readable region. + /// The read operation should succeed (not throw), and the bytes that are readable should be read. + /// + [Test] + public void ReadOnTheEdgeOfValidMemoryTest() + { + // Prepare a segment of memory that is isolated from other memory regions, and has a known sequence of bytes + // at the end. + var bytesAtTheEnd = new byte[] { 0x1, 0x2, 0x3, 0x4 }; + var allocatedMemory = TestProcessMemory!.Allocate(0x1000, false).Value; + var targetAddress = allocatedMemory.Range.End - 4; + var writeResult = TestProcessMemory.WriteBytes(targetAddress, bytesAtTheEnd, MemoryProtectionStrategy.Ignore); + Assert.That(writeResult.IsSuccess, Is.True); + + // Attempt to read 8 bytes from the target address, which is 4 bytes before the end of the isolated segment. + using var stream = TestProcessMemory.GetMemoryStream(targetAddress); + var buffer = new byte[8]; + int byteCount = stream.Read(buffer, 0, 8); + + // We should have read only 4 bytes, and the first 4 bytes of the buffer should be the bytes we wrote at the end + // of our memory segment. + Assert.That(byteCount, Is.EqualTo(4)); + Assert.That(buffer.Take(4), Is.EqualTo(bytesAtTheEnd)); + } + + /// + /// Tests the method. + /// Performs a read operation that starts in an unreadable region. + /// The read operation should succeed (not throw) but return that 0 bytes were read. + /// + [Test] + public void ReadOnUnreadableMemoryTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(UIntPtr.MaxValue); + var buffer = new byte[8]; + int byteCount = stream.Read(buffer, 0, 8); + + Assert.That(byteCount, Is.Zero); + } + + #endregion + + #region Write + + /// + /// Tests the method. + /// Performs a single write operation and read back that value using a process memory read method. + /// Must read back the data that was written from the stream. + /// + [Test] + public void SimpleWriteTest() + { + var address = OuterClassPointer + 0x28; + using var stream = TestProcessMemory!.GetMemoryStream(address); + var buffer = new byte[] { 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7 }; + stream.Write(buffer, 0, 8); + var readValue = TestProcessMemory.ReadBytes(address, 8).Value; + + Assert.That(readValue, Is.EqualTo(buffer)); + } + + /// + /// Tests the method. + /// Performs two write operations over known values, and read back the data using a process memory read method. + /// Must read back the data that was written from the stream. + /// This tests that the write methods write from the current position in the stream, and not always from the start. + /// + [Test] + public void MultipleWriteTest() + { + var startAddress = OuterClassPointer + 0x20; + using var stream = TestProcessMemory!.GetMemoryStream(startAddress); + var buffer = new byte[] { 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7 }; + stream.Write(buffer, 0, 8); + stream.Write(buffer, 0, 8); + var readValue = TestProcessMemory.ReadBytes(startAddress, 16).Value; + + Assert.That(readValue, Is.EqualTo(buffer.Concat(buffer))); + } + + /// + /// Tests the method. + /// Performs a single write operation, with an offset, and read back that value using a process memory read method. + /// Must read back only the portion of the data that starts at the offset in the written buffer. + /// + [Test] + public void WriteWithOffsetTest() + { + var startAddress = OuterClassPointer + 0x28; + using var stream = TestProcessMemory!.GetMemoryStream(startAddress); + var buffer = new byte[] + { + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, + 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF + }; + stream.Write(buffer, 8, 8); // Use an offset of 8, which should write from 0x8 to 0xF + var readValue = TestProcessMemory.ReadBytes(startAddress, 8).Value; + + Assert.That(readValue, Is.EqualTo(new byte[] { 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF })); + } + + /// + /// Tests the method. + /// Calls the read method with an offset that goes beyond the capacity of the buffer. + /// Must throw a . + /// + [Test] + public void WriteWithImpossibleOffsetTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(() => stream.Write(new byte[8], 8, 1), Throws.InstanceOf()); + } + + /// + /// Tests the method. + /// Calls the read method with a count that goes beyond the capacity of the buffer. + /// Must throw a . + /// + [Test] + public void WriteWithImpossibleCountTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(() => stream.Write(new byte[8], 0, 9), Throws.InstanceOf()); + } + + /// + /// Tests the method. + /// Calls the read method with a combination of offset and count that go beyond the capacity of the buffer. + /// Must throw a . + /// + [Test] + public void WriteWithImpossibleOffsetAndCountTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + Assert.That(() => stream.Write(new byte[8], 3, 6), Throws.InstanceOf()); + } + + /// + /// Tests the method. + /// Performs a write operation that starts in a writable region, but ends in a non-writable region. + /// The write operation should fail with an . + /// + [Test] + public void WriteOnTheEdgeOfValidMemoryTest() + { + // Prepare a segment of memory that is isolated from other memory regions, and has a known sequence of bytes + // at the end. + var bytesAtTheEnd = new byte[] { 0x1, 0x2, 0x3, 0x4 }; + var allocatedMemory = TestProcessMemory!.Allocate(0x1000, false).Value; + var targetAddress = allocatedMemory.Range.End - 4; + var writeResult = TestProcessMemory.WriteBytes(targetAddress, bytesAtTheEnd, MemoryProtectionStrategy.Ignore); + Assert.That(writeResult.IsSuccess, Is.True); + + // Attempt to write 8 bytes from the target address, which is 4 bytes before the end of the isolated segment. + using var stream = TestProcessMemory.GetMemoryStream(targetAddress); + Assert.That(() => stream.Write(new byte[8], 0, 8), Throws.InstanceOf()); + } + + /// + /// Tests the method. + /// Performs a write operation that starts in an unwritable region. + /// The write operation should fail with an . + /// + [Test] + public void WriteOnUnreadableMemoryTest() + { + using var stream = TestProcessMemory!.GetMemoryStream(UIntPtr.MaxValue); + Assert.That(() => stream.Write(new byte[8], 0, 8), Throws.InstanceOf()); + } + + #endregion +} \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs index 8e13526..b1ea6ec 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs @@ -8,8 +8,6 @@ namespace MindControl.Test.ProcessMemoryTests; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ProcessMemoryFindBytesTest : ProcessMemoryTest { - private const string MainModuleName = "MindControl.Test.TargetApp.dll"; - /// /// Tests the method with a known fixed bytes pattern. /// The search is performed in the main module of the target process, with default search options. diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs index 6a224be..07158b2 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs @@ -13,6 +13,8 @@ namespace MindControl.Test.ProcessMemoryTests; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ProcessMemoryTest { + protected const string MainModuleName = "MindControl.Test.TargetApp.dll"; + protected Process? _targetProcess; protected ProcessMemory? TestProcessMemory; protected UIntPtr OuterClassPointer; From d326a0bb850cc48eb42e6094bc59bb4d69d5099b Mon Sep 17 00:00:00 2001 From: Doublevil Date: Thu, 6 Jun 2024 15:26:29 +0200 Subject: [PATCH 15/66] Full string reading implementation --- .../ProcessMemory/ProcessMemory.Read.cs | 280 +++++- src/MindControl/Results/ReadFailure.cs | 19 +- src/MindControl/Results/StringReadFailure.cs | 138 +++ src/MindControl/StringSettings.cs | 73 +- .../ProcessMemoryReadTest.cs | 794 ++++++++++++++---- 5 files changed, 1072 insertions(+), 232 deletions(-) create mode 100644 src/MindControl/Results/StringReadFailure.cs diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs index 1c9dfd2..5635270 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs @@ -9,10 +9,11 @@ namespace MindControl; public partial class ProcessMemory { /// - /// Gets or sets the default maximum length of strings to read when the length is not specified. - /// The default value is a completely arbitrary 100. + /// Gets or sets the default maximum length of strings to read with + /// when the length is not specified. + /// The default value is an arbitrary 100. /// - public int DefaultMaxStringLength { get; set; } = 100; + public int DefaultRawStringMaxLength { get; set; } = 100; #region Bytes reading @@ -23,7 +24,12 @@ public partial class ProcessMemory /// Number of bytes to read. /// The value read from the process memory, or a read failure. public Result ReadBytes(PointerPath pointerPath, long length) - => ReadBytes(pointerPath, (ulong)length); + { + if (length < 0) + return new ReadFailureOnInvalidArguments("The length to read cannot be negative."); + + return ReadBytes(pointerPath, (ulong)length); + } /// /// Reads a sequence of bytes from the address referred by the given pointer path, in the process memory. @@ -45,8 +51,13 @@ public Result ReadBytes(PointerPath pointerPath, ulong leng /// Number of bytes to read. /// The value read from the process memory, or a read failure. public Result ReadBytes(UIntPtr address, long length) - => ReadBytes(address, (ulong)length); - + { + if (length < 0) + return new ReadFailureOnInvalidArguments("The length to read cannot be negative."); + + return ReadBytes(address, (ulong)length); + } + /// /// Reads a sequence of bytes from the given address in the process memory. /// @@ -65,6 +76,51 @@ public Result ReadBytes(UIntPtr address, ulong length) return readResult.IsSuccess ? readResult.Value : new ReadFailureOnSystemRead(readResult.Error); } + + /// + /// Reads a sequence of bytes from the address pointed by the given path in the process memory. The resulting bytes + /// are read into the provided buffer array. Unlike , this method succeeds even + /// when only a part of the bytes are read. + /// Use it when you are not sure how many bytes you need to read, or when you want to read as many bytes as + /// possible. + /// + /// Pointer path to the target address in the process memory. + /// Buffer to store the bytes read. + /// Number of bytes to read, at most. + /// The value read from the process memory, or a read failure in case no bytes could be read. + public Result ReadBytesPartial(PointerPath pointerPath, byte[] buffer, ulong maxLength) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadBytesPartial(addressResult.Value, buffer, maxLength) + : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + } + + /// + /// Reads a sequence of bytes from the given address in the process memory. The resulting bytes are read into the + /// provided buffer array. Unlike , this method succeeds even when only a part + /// of the bytes are read. + /// Use it when you are not sure how many bytes you need to read, or when you want to read as many bytes as + /// possible. + /// + /// Target address in the process memory. + /// Buffer to store the bytes read. + /// Number of bytes to read, at most. + /// The value read from the process memory, or a read failure in case no bytes could be read. + public Result ReadBytesPartial(UIntPtr address, byte[] buffer, ulong maxLength) + { + if (maxLength == 0) + return 0; + if ((ulong)buffer.Length < maxLength) + return new ReadFailureOnInvalidArguments("The buffer length must be at least the provided length to read."); + if (address == UIntPtr.Zero) + return new ReadFailureOnZeroPointer(); + if (!IsBitnessCompatible(address)) + return new ReadFailureOnIncompatibleBitness(address); + + var readResult = _osService.ReadProcessMemoryPartial(_processHandle, address, buffer, 0, maxLength); + return readResult.IsSuccess ? readResult.Value + : new ReadFailureOnSystemRead(readResult.Error); + } #endregion @@ -220,8 +276,8 @@ public Result Read(Type type, UIntPtr address) /// Length prefix units to try when trying to find the length prefix of a string, in order of priority. /// - private static readonly StringLengthPrefixUnit[] FindStringLengthPrefixUnits = - { StringLengthPrefixUnit.Bytes, StringLengthPrefixUnit.Characters }; + private static readonly StringLengthUnit[] FindStringLengthPrefixUnits = + { StringLengthUnit.Bytes, StringLengthUnit.Characters }; /// Null-termination settings to try when trying to find the null-termination of a string, in order of /// priority. @@ -304,7 +360,7 @@ public Result FindStringSettings(UInt // This is important not only for performance, but also for accuracy (to return the most probable match first, // even when multiple combinations work). // For instance, the type prefix loop comes first, and should have a value of 0 at first, in order to prevent - // false positives where the type prefix wrongly contains the length prefix. + // false positives where the type prefix mistakenly contains the length prefix. foreach (int typePrefixSize in FindStringTypePrefixSizes) { foreach (var encoding in FindStringEncodings) @@ -356,8 +412,8 @@ public Result FindStringSettings(UInt /// /// Reads a string from the address referred by the given pointer path, in the process memory. - /// The address must point to the start of the actual string bytes. Consider to - /// read strings from pointers and with added capabilities. + /// The address must point to the start of the actual string bytes. Consider + /// to read strings from pointers more efficiently. /// Read the documentation for more information. /// /// Path to the first byte of the raw string in the process memory. @@ -366,7 +422,7 @@ public Result FindStringSettings(UInt /// . /// /// Maximum length of the string to read, in characters. If left null (default), the - /// will be used. + /// will be used. /// Boolean indicating if the string is null-terminated. If true, the string will be /// read until the first null character. If false, the string will be read up to the maximum length specified. /// @@ -381,8 +437,8 @@ public Result ReadRawString(PointerPath pointerPath, Encodi /// /// Reads a string from the given address in the process memory. - /// The address must point to the start of the actual string bytes. Consider to - /// read strings from pointers and with added capabilities. + /// The address must point to the start of the actual string bytes. Consider + /// to read strings from pointers more efficiently. /// Read the documentation for more information. /// /// Address of the first byte of the raw string in the process memory. @@ -391,7 +447,7 @@ public Result ReadRawString(PointerPath pointerPath, Encodi /// . /// /// Maximum length of the string to read, in characters. If left null (default), the - /// will be used. + /// will be used. /// Boolean indicating if the string is null-terminated. If true, the string will be /// read until the first null character. If false, the string will be read up to the maximum length specified. /// @@ -400,26 +456,32 @@ public Result ReadRawString(UIntPtr address, Encoding encod int? maxLength = null, bool isNullTerminated = true) { if (maxLength is < 0) - throw new ArgumentOutOfRangeException(nameof(maxLength), "The maximum length cannot be negative."); + return new ReadFailureOnInvalidArguments("The maximum length cannot be negative."); + if (maxLength == 0) + return string.Empty; // We don't know how many bytes the string will take, because encodings can have variable byte sizes. // So we read the maximum amount of bytes that the string could take, and then cut it to the max length. // Calculate the maximum byte size to read - maxLength = maxLength ?? DefaultMaxStringLength; + maxLength = maxLength ?? DefaultRawStringMaxLength; int byteSizeToRead = encoding.GetMaxByteCount(maxLength.Value) + (isNullTerminated ? encoding.GetNullTerminator().Length : 0); - // Read the bytes - //todo: Use a read method that handles cases where bytes are only partially read, because we don't want the - //operation to fail if we read a string that's at the end of a region and the next region is not readable. - var readResult = ReadBytes(address, (ulong)byteSizeToRead); + // Read the bytes using a buffer (in case we can't read the whole max size) + var buffer = new byte[byteSizeToRead]; + var readResult = ReadBytesPartial(address, buffer, (ulong)byteSizeToRead); if (readResult.IsFailure) return readResult.Error; + // Check the number of bytes read + ulong readByteCount = readResult.Value; + if (readByteCount == 0) + return string.Empty; + // Convert the bytes to a string - byte[] bytes = readResult.Value; - string? result = new StringSettings(encoding, isNullTerminated).GetString(bytes); + var readBytes = buffer.AsSpan(0, (int)readByteCount); + string? result = new StringSettings(encoding, isNullTerminated).GetString(readBytes); // Cut the string to the max length if needed if (result?.Length > maxLength) @@ -428,6 +490,22 @@ public Result ReadRawString(UIntPtr address, Encoding encod return result ?? string.Empty; } + /// + /// Reads the string pointed by the pointer evaluated from the given pointer path from the process memory. + /// This method uses a instance to determine how to read the string. + /// + /// Pointer path to the pointer to the string in the process memory. + /// Settings that define how to read the string. If you cannot figure out what settings to + /// use, try to automatically determine the right settings for a + /// known string pointer. See the documentation for more information. + /// The string read from the process memory, or a read failure. + public Result ReadStringPointer(PointerPath pointerPath, StringSettings settings) + { + var addressResult = EvaluateMemoryAddress(pointerPath); + return addressResult.IsSuccess ? ReadStringPointer(addressResult.Value, settings) + : new StringReadFailureOnPointerPathEvaluation(addressResult.Error); + } + /// /// Reads the string pointed by the pointer at the given address from the process memory. /// This method uses a instance to determine how to read the string. @@ -437,21 +515,157 @@ public Result ReadRawString(UIntPtr address, Encoding encod /// use, try to automatically determine the right settings for a /// known string pointer. See the documentation for more information. /// The string read from the process memory, or a read failure. - public Result ReadStringPointer(UIntPtr address, StringSettings settings) + public Result ReadStringPointer(UIntPtr address, StringSettings settings) { + if (!IsBitnessCompatible(address)) + return new StringReadFailureOnIncompatibleBitness(address); + if (address == UIntPtr.Zero) + return new StringReadFailureOnZeroPointer(); + if (!settings.IsValid) + return new StringReadFailureOnInvalidSettings(); + + // Start by reading the address of the string bytes + var stringAddressResult = Read(address); + if (stringAddressResult.IsFailure) + return new StringReadFailureOnPointerReadFailure(stringAddressResult.Error); + if (stringAddressResult.Value == UIntPtr.Zero) + return new StringReadFailureOnZeroPointer(); + var stringAddress = stringAddressResult.Value; + // The string settings will either have a null terminator or a length prefix. - // The reading strategy depends on which of these two we have. + // The length prefix can also either be in bytes or in characters. + // That leaves us with 3 potential ways to read the string. + // Each way will have a different implementation. + + // In case we have both a length prefix and a null terminator, we will prioritize the length prefix, as it is + // more reliable and usually more efficient. + + if (settings.LengthPrefix?.Unit == StringLengthUnit.Bytes) + return ReadStringWithLengthPrefixInBytes(stringAddress, settings); + if (settings.LengthPrefix?.Unit == StringLengthUnit.Characters) + return ReadStringWithLengthPrefixInCharacters(stringAddress, settings); + + // If we reach this point, we have a null-terminated string, because settings must have either a length prefix + // or a null terminator, otherwise they're not valid and already caused a failure. + return ReadStringWithNullTerminator(stringAddress, settings); + } + + /// + /// Reads a string starting at the given address, using the specified settings, and assuming that the settings + /// have a length prefix in bytes. + /// + /// Address of the string in the process memory. + /// Settings that define how to read the string. + /// The string read from the process memory, or a read failure. + private Result ReadStringWithLengthPrefixInBytes(UIntPtr address, + StringSettings settings) + { + // The strategy when the string has a length prefix in bytes is the easiest one: + // Read the length prefix, then we know exactly how many bytes to read, and then use the encoding to decode + // the bytes read. + + // Read the length prefix + int lengthPrefixOffset = settings.TypePrefix?.Length ?? 0; + var lengthPrefixAddress = (UIntPtr)(address.ToUInt64() + (ulong)lengthPrefixOffset); + var lengthPrefixBytesResult = ReadBytes(lengthPrefixAddress, settings.LengthPrefix!.Size); + if (lengthPrefixBytesResult.IsFailure) + return new StringReadFailureOnStringBytesReadFailure(lengthPrefixAddress, lengthPrefixBytesResult.Error); + ulong length = lengthPrefixBytesResult.Value.ReadUnsignedNumber(); - // If we have a null terminator, read small chunks of bytes until we find a null terminator. - // For this, we may need to make an alternative way to read bytes that returns a custom stream. The stream - // would end whenever we reach unreadable memory. - // In this method, we would read from the stream until we find a null terminator. - // Multi-bytes null terminators could be handled this way too, because if we get a sequence of 2 null bytes, - // we know that the null terminator is either at the start of the sequence, or the start of the sequence + 1. + if (length == 0) + return string.Empty; + if (length > (ulong)settings.MaxLength) + return new StringReadFailureOnStringTooLong(length); - // If we have a length prefix, read the length prefix and then read the string bytes at once. + // Read the string bytes (after the prefixes) + int stringBytesOffset = lengthPrefixOffset + settings.LengthPrefix.Size; + var stringBytesAddress = (UIntPtr)(address.ToUInt64() + (ulong)stringBytesOffset); + var stringBytesResult = ReadBytes(stringBytesAddress, length); + if (stringBytesResult.IsFailure) + return new StringReadFailureOnStringBytesReadFailure(stringBytesAddress, lengthPrefixBytesResult.Error); + var stringBytes = stringBytesResult.Value; + + // Decode the bytes into a string using the encoding specified in the settings + return settings.Encoding.GetString(stringBytes); + } + + /// + /// Reads a string starting at the given address, using the specified settings, and assuming that the settings + /// have a length prefix in characters. + /// + /// Address of the string in the process memory. + /// Settings that define how to read the string. + /// The string read from the process memory, or a read failure. + private Result ReadStringWithLengthPrefixInCharacters(UIntPtr address, + StringSettings settings) + { + // The strategy when the string has a length prefix in characters is the following: + // Read the length prefix, then get a stream that reads bytes from the process memory, and use a stream reader + // to read characters until we reach the length specified by the length prefix. + + // Read the length prefix + int lengthPrefixOffset = settings.TypePrefix?.Length ?? 0; + var lengthPrefixAddress = (UIntPtr)(address.ToUInt64() + (ulong)lengthPrefixOffset); + var lengthPrefixBytesResult = ReadBytes(lengthPrefixAddress, settings.LengthPrefix!.Size); + if (lengthPrefixBytesResult.IsFailure) + return new StringReadFailureOnStringBytesReadFailure(lengthPrefixAddress, lengthPrefixBytesResult.Error); + ulong expectedStringLength = lengthPrefixBytesResult.Value.ReadUnsignedNumber(); + + if (expectedStringLength == 0) + return string.Empty; + if (expectedStringLength > (ulong)settings.MaxLength) + return new StringReadFailureOnStringTooLong(expectedStringLength); + + // Get a memory stream after the prefixes + int stringBytesOffset = lengthPrefixOffset + settings.LengthPrefix.Size; + using var stream = GetMemoryStream(address + stringBytesOffset); + using var streamReader = new StreamReader(stream, settings.Encoding); + + // Read characters until we reach the expected length + var stringBuilder = new StringBuilder((int)expectedStringLength); + for (ulong i = 0; i < expectedStringLength; i++) + { + int nextChar = streamReader.Read(); + + // If we reach the end of the stream, return the string we have so far + if (nextChar == -1) + return stringBuilder.ToString(); + + stringBuilder.Append((char)nextChar); + } + + return stringBuilder.ToString(); + } + + /// + /// Reads a string starting at the given address, using the specified settings, and assuming that the settings + /// have a null terminator. + /// + /// Address of the string in the process memory. + /// Settings that define how to read the string. + /// The string read from the process memory, or a read failure. + private Result ReadStringWithNullTerminator(UIntPtr address, StringSettings settings) + { + // The strategy when the string has no length prefix but has a null-terminator is the following: + // Get a stream that reads bytes from the process memory, and use a stream reader to read characters until we + // find a null terminator. + + // Get a memory stream after the prefixes + int stringBytesOffset = (settings.TypePrefix?.Length ?? 0) + (settings.LengthPrefix?.Size ?? 0); + using var stream = GetMemoryStream(address + stringBytesOffset); + using var streamReader = new StreamReader(stream, settings.Encoding); + + // Read characters until we reach a null terminator (0) or the end of the stream (-1) + var stringBuilder = new StringBuilder(16); // Arbitrary initial size because we don't know the string length + for (var nextChar = streamReader.Read(); nextChar != -1 && nextChar != 0; nextChar = streamReader.Read()) + { + stringBuilder.Append((char)nextChar); + + if (stringBuilder.Length > settings.MaxLength) + return new StringReadFailureOnStringTooLong(null); + } - throw new NotImplementedException(); + return stringBuilder.ToString(); } #endregion diff --git a/src/MindControl/Results/ReadFailure.cs b/src/MindControl/Results/ReadFailure.cs index e39dc2c..0019853 100644 --- a/src/MindControl/Results/ReadFailure.cs +++ b/src/MindControl/Results/ReadFailure.cs @@ -6,7 +6,12 @@ public enum ReadFailureReason { /// - /// Failure when evaluating the pointer path to the target memory. + /// The arguments provided to the memory read operation are invalid. + /// + InvalidArguments, + + /// + /// Failure when evaluating the pointer path to the target address. /// PointerPathEvaluationFailure, @@ -42,6 +47,18 @@ public enum ReadFailureReason /// Reason for the failure. public abstract record ReadFailure(ReadFailureReason Reason); +/// +/// Represents a failure in a memory read operation when the arguments provided are invalid. +/// +/// Message that describes how the arguments fail to meet expectations. +public record ReadFailureOnInvalidArguments(string Message) + : ReadFailure(ReadFailureReason.InvalidArguments) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The arguments provided are invalid: {Message}"; +} + /// /// Represents a failure in a memory read operation when evaluating the pointer path to the target memory. /// diff --git a/src/MindControl/Results/StringReadFailure.cs b/src/MindControl/Results/StringReadFailure.cs new file mode 100644 index 0000000..2c26fd4 --- /dev/null +++ b/src/MindControl/Results/StringReadFailure.cs @@ -0,0 +1,138 @@ +namespace MindControl.Results; + +/// +/// Represents a reason for a string read operation to fail. +/// +public enum StringReadFailureReason +{ + /// + /// Failure when evaluating the pointer path to the target address. + /// + PointerPathEvaluationFailure, + + /// + /// The string read operation failed because the settings provided are invalid. + /// + InvalidSettings, + + /// + /// The string read operation failed because the target process is 32-bits, but the target memory address is not + /// within the 32-bit address space. + /// + IncompatibleBitness, + + /// + /// The string read operation failed because the target pointer is a zero pointer. + /// + ZeroPointer, + + /// + /// The pointer read operation failed. + /// + PointerReadFailure, + + /// + /// A read operation failed when attempting to read bytes from the actual string. + /// + StringBytesReadFailure, + + /// + /// The length prefix of the string was evaluated to a value exceeding the configured max length, or a null + /// terminator was not found within the configured max length. + /// + StringTooLong +} + +/// +/// Represents a failure in a string read operation. +/// +/// Reason for the failure. +public abstract record StringReadFailure(StringReadFailureReason Reason); + +/// +/// Represents a failure in a string read operation when the pointer path evaluation failed. +/// +/// Details about the path evaluation failure. +public record StringReadFailureOnPointerPathEvaluation(PathEvaluationFailure Details) + : StringReadFailure(StringReadFailureReason.PointerPathEvaluationFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failed to evaluate the specified pointer path: {Details}"; +} + +/// +/// Represents a failure in a string read operation when the settings provided are invalid. +/// +public record StringReadFailureOnInvalidSettings() + : StringReadFailure(StringReadFailureReason.InvalidSettings) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => "The provided string settings are invalid. They must specify either a length prefix or a null terminator."; +} + +/// +/// Represents a failure in a string read operation when the target process is 32-bits, but the target memory address is +/// not within the 32-bit address space. +/// +/// Address that caused the failure. +public record StringReadFailureOnIncompatibleBitness(UIntPtr Address) + : StringReadFailure(StringReadFailureReason.IncompatibleBitness) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The target address {Address} is not within the 32-bit address space."; +} + +/// +/// Represents a failure in a string read operation when the target pointer is a zero pointer. +/// +public record StringReadFailureOnZeroPointer() + : StringReadFailure(StringReadFailureReason.ZeroPointer) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => "The target address is a zero pointer."; +} + +/// +/// Represents a failure in a string read operation when the pointer read operation failed. +/// +/// Details about the pointer read failure. +public record StringReadFailureOnPointerReadFailure(ReadFailure Details) + : StringReadFailure(StringReadFailureReason.PointerReadFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The pointer read operation failed: {Details}"; +} + +/// +/// Represents a failure in a string read operation when a read operation on the string bytes failed. +/// +/// Address where the string read operation failed. +/// Details about the read failure. +public record StringReadFailureOnStringBytesReadFailure(UIntPtr Address, ReadFailure Details) + : StringReadFailure(StringReadFailureReason.StringBytesReadFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The string read operation failed at address {Address}: {Details}"; +} + +/// +/// Represents a failure in a string read operation when the string length prefix was evaluated to a value exceeding the +/// configured max length, or a null terminator was not found within the configured max length. +/// +/// Length read from the length prefix bytes, in case a length prefix was set. +public record StringReadFailureOnStringTooLong(ulong? LengthPrefixValue) + : StringReadFailure(StringReadFailureReason.StringTooLong) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => LengthPrefixValue != null + ? $"The string was found with a length prefix of {LengthPrefixValue}, which exceeds the configured max length." + : "String reading was aborted because no null terminator was found within the configured max length."; +} diff --git a/src/MindControl/StringSettings.cs b/src/MindControl/StringSettings.cs index 728a5ca..5e1e093 100644 --- a/src/MindControl/StringSettings.cs +++ b/src/MindControl/StringSettings.cs @@ -9,6 +9,21 @@ namespace MindControl; /// public class StringSettings { + /// + /// Default value for . + /// + public const int DefaultMaxLength = 1024; + + /// + /// Gets or sets the maximum length of the strings that can be read with this instance. + /// When reading strings, if the length of the string is evaluated to a value exceeding the maximum, the read + /// operation will be aborted and fail to prevent reading unexpected data. + /// The unit of this value depends on the settings. If no length prefix is used, the unit + /// used is the number of characters in the string. + /// The default value is defined by the constant. + /// + public int MaxLength { get; set; } = DefaultMaxLength; + /// /// Gets or sets an optional prefix that comes before the string bytes. This is useful for type pointers in /// frameworks that use them. If the string also has a length prefix, the type prefix comes first, before the @@ -39,8 +54,9 @@ public class StringSettings /// /// Builds settings with the given properties. - /// If you are unsure what settings to use, consider using to - /// automatically determine the appropriate settings for a known string pointer. + /// If you are unsure what settings to use, consider using + /// if possible to automatically determine the + /// appropriate settings for a known string pointer. /// /// Encoding of the strings. /// Boolean indicating if strings should have a \0 delimitation character at the end. @@ -81,7 +97,7 @@ public int GetMaxByteLength(int maxStringLength) ThrowIfInvalid(); return Encoding.GetMaxByteCount(maxStringLength) - + (LengthPrefix?.PrefixSize ?? 0) + + (LengthPrefix?.Size ?? 0) + (TypePrefix?.Length ?? 0) + (IsNullTerminated ? Encoding.GetNullTerminator().Length : 0); } @@ -108,11 +124,11 @@ public int GetMaxByteLength(int maxStringLength) { lengthToWrite = LengthPrefix.Unit switch { - StringLengthPrefixUnit.Characters => value.Length, + StringLengthUnit.Characters => value.Length, _ => byteCount }; - switch (LengthPrefix.PrefixSize) + switch (LengthPrefix.Size) { case 1 when lengthToWrite > byte.MaxValue: case 2 when lengthToWrite > short.MaxValue: @@ -133,16 +149,16 @@ public int GetMaxByteLength(int maxStringLength) { // Write the length into the byte array, after the type prefix if any var span = bytes.AsSpan(currentIndex); - if (LengthPrefix!.PrefixSize == 1) + if (LengthPrefix!.Size == 1) span[0] = (byte)lengthToWrite.Value; - else if (LengthPrefix.PrefixSize == 2) + else if (LengthPrefix.Size == 2) BinaryPrimitives.WriteInt16LittleEndian(span, (short)lengthToWrite.Value); - else if (LengthPrefix.PrefixSize == 4) + else if (LengthPrefix.Size == 4) BinaryPrimitives.WriteInt32LittleEndian(span, lengthToWrite.Value); else BinaryPrimitives.WriteInt64LittleEndian(span, lengthToWrite.Value); - currentIndex += LengthPrefix.PrefixSize; + currentIndex += LengthPrefix.Size; } // Write the string bytes. Encoder.Convert is used to avoid unnecessary allocations. @@ -166,7 +182,7 @@ public int GetByteCount(string value) ThrowIfInvalid(); return (TypePrefix?.Length ?? 0) - + (LengthPrefix?.PrefixSize ?? 0) + + (LengthPrefix?.Size ?? 0) + Encoding.GetByteCount(value) + (IsNullTerminated ? Encoding.GetNullTerminator().Length : 0); } @@ -176,10 +192,12 @@ public int GetByteCount(string value) /// /// Bytes to read the string from. /// The string read from the bytes, or null if the string could not be read. - public string? GetString(byte[] bytes) + /// This method ignores the constraint, because the full span of bytes is already + /// provided as a parameter. + public string? GetString(Span bytes) { // Figure out the start index of the actual string bytes, after the prefixes (if any) - int lengthPrefixSize = LengthPrefix?.PrefixSize ?? 0; + int lengthPrefixSize = LengthPrefix?.Size ?? 0; int typePrefixSize = TypePrefix?.Length ?? 0; int startIndex = lengthPrefixSize + typePrefixSize; @@ -192,9 +210,9 @@ public int GetByteCount(string value) // If the length prefix is in bytes, read the length prefix and use it as the length to read. // If we have a null terminator, we also have to read it, so add its length to the length to read. // Otherwise, read the remaining bytes. - bool hasBytesLengthPrefix = LengthPrefix is { PrefixSize: > 0, Unit: StringLengthPrefixUnit.Bytes }; + bool hasBytesLengthPrefix = LengthPrefix is { Size: > 0, Unit: StringLengthUnit.Bytes }; ulong lengthToRead = hasBytesLengthPrefix ? - bytes.AsSpan(typePrefixSize, lengthPrefixSize).ReadUnsignedNumber() + bytes.Slice(typePrefixSize, lengthPrefixSize).ReadUnsignedNumber() + (ulong)(IsNullTerminated ? Encoding.GetNullTerminator().Length : 0) : (ulong)remainingBytes; @@ -209,15 +227,15 @@ public int GetByteCount(string value) return string.Empty; // Read the string bytes using the encoding in the settings - var stringBytes = bytes.AsSpan(startIndex, (int)lengthToRead); + var stringBytes = bytes.Slice(startIndex, (int)lengthToRead); if (stringBytes.Length == 0) return null; string resultingString = Encoding.GetString(stringBytes); // If we have a length prefix in characters, we have to cut the string to the correct length - if (LengthPrefix is { PrefixSize: > 0, Unit: StringLengthPrefixUnit.Characters }) + if (LengthPrefix is { Size: > 0, Unit: StringLengthUnit.Characters }) { - ulong characterLength = bytes.AsSpan(typePrefixSize, lengthPrefixSize).ReadUnsignedNumber(); + ulong characterLength = bytes.Slice(typePrefixSize, lengthPrefixSize).ReadUnsignedNumber(); if (IsNullTerminated) characterLength++; @@ -229,12 +247,11 @@ public int GetByteCount(string value) } // If the string is null-terminated, we have to cut the string at the null-terminator - // If there is no null-terminator, return null, as this means the string settings and byte array are not - // compatible. if (IsNullTerminated) { int nullTerminatorIndex = resultingString.IndexOf('\0'); - return nullTerminatorIndex >= 0 ? resultingString[..nullTerminatorIndex] : null; + return nullTerminatorIndex >= 0 ? resultingString[..nullTerminatorIndex] + : resultingString; // If there is no null terminator, we choose to return the full string } // No length prefix, or length prefix in bytes. Return the full string. @@ -245,7 +262,7 @@ public int GetByteCount(string value) /// /// Defines what is counted by a string length prefix. /// -public enum StringLengthPrefixUnit +public enum StringLengthUnit { /// /// The length prefix is a count of characters. @@ -266,24 +283,24 @@ public class StringLengthPrefix /// /// Gets or sets the number of bytes storing the length of the string. /// - public int PrefixSize { get; } + public int Size { get; } /// /// Gets or sets what the length prefix counts. /// - public StringLengthPrefixUnit Unit { get; } + public StringLengthUnit Unit { get; } /// /// Builds length prefix settings with the given properties. /// - /// Number of bytes storing the length of the string. + /// Number of bytes storing the length of the string. /// What the length prefix counts. - public StringLengthPrefix(int prefixSize, StringLengthPrefixUnit unit) + public StringLengthPrefix(int size, StringLengthUnit unit) { - if (prefixSize != 1 && prefixSize != 2 && prefixSize != 4 && prefixSize != 8) - throw new ArgumentOutOfRangeException(nameof(prefixSize), "Prefix size must be either 1, 2, 4 or 8."); + if (size != 1 && size != 2 && size != 4 && size != 8) + throw new ArgumentOutOfRangeException(nameof(size), "Prefix size must be either 1, 2, 4 or 8."); - PrefixSize = prefixSize; + Size = size; Unit = unit; } } diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index ea3a4af..9de8ee8 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -10,6 +10,232 @@ namespace MindControl.Test.ProcessMemoryTests; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ProcessMemoryReadTest : ProcessMemoryTest { + #region Bytes reading + + #region ReadBytes + + /// + /// Tests . + /// Reads a known byte array from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. + /// + [Test] + public void ReadBytesTest() + { + PointerPath pointerPath = $"{OuterClassPointer:X}+18,10"; + Assert.That(TestProcessMemory!.ReadBytes(pointerPath, 4).GetValueOrDefault(), + Is.EqualTo(new byte[] { 0x11, 0x22, 0x33, 0x44 })); + ProceedToNextStep(); + Assert.That(TestProcessMemory.ReadBytes(pointerPath, 4).GetValueOrDefault(), + Is.EqualTo(new byte[] { 0x55, 0x66, 0x77, 0x88 })); + } + + /// + /// Tests with a zero pointer. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesWithZeroPointerTest() + { + var result = TestProcessMemory!.ReadBytes(UIntPtr.Zero, 4); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnZeroPointer))); + } + + /// + /// Tests with an unreadable address. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesWithUnreadableAddressTest() + { + var result = TestProcessMemory!.ReadBytes(UIntPtr.MaxValue, 1); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); + var systemError = ((ReadFailureOnSystemRead)result.Error).Details; + Assert.That(systemError, Is.Not.Null); + } + + /// + /// Tests with a bad pointer path. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesWithInvalidPathTest() + { + var result = TestProcessMemory!.ReadBytes($"{OuterClassPointer:X}+10,10,0,0", 4); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); + var pathError = ((ReadFailureOnPointerPathEvaluation)result.Error).Details; + Assert.That(pathError, Is.Not.Null); + } + + /// + /// Tests with a zero length. + /// Expect the result to be an empty array of bytes. + /// + [Test] + public void ReadBytesWithZeroLengthTest() + { + var result = TestProcessMemory!.ReadBytes(OuterClassPointer, 0); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.Empty); + } + + /// + /// Tests with a negative length. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesWithNegativeLengthTest() + { + var result = TestProcessMemory!.ReadBytes(OuterClassPointer, -4); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnInvalidArguments))); + } + + /// + /// Tests with an address and length that would make the read + /// start on a readable address but end on an unreadable one. + /// Expect the result to be a . + /// + /// This is what is for. + [Test] + public void ReadBytesOnPartiallyUnreadableRangeTest() + { + // Prepare a segment of memory that is isolated from other memory regions, and get an address near the edge. + var allocatedMemory = TestProcessMemory!.Allocate(0x1000, false).Value; + var targetAddress = allocatedMemory.Range.End - 4; + + // Read 8 bytes, which should result in reading 4 bytes from the readable region and 4 bytes from the unreadable + // one. + var result = TestProcessMemory.ReadBytes(targetAddress, 8); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); + } + + #endregion + + #region ReadBytesPartial + + /// + /// Tests . + /// Reads a known byte array from the target process before and after the process changes their value. + /// It should be equal to its known values before and after modification. + /// + [Test] + public void ReadBytesPartialTest() + { + PointerPath pointerPath = $"{OuterClassPointer:X}+18,10"; + var firstBuffer = new byte[4]; + var firstResult = TestProcessMemory!.ReadBytesPartial(pointerPath, firstBuffer, 4); + ProceedToNextStep(); + var secondBuffer = new byte[4]; + var secondResult = TestProcessMemory.ReadBytesPartial(pointerPath, secondBuffer, 4); + + Assert.That(firstResult.IsSuccess); + Assert.That(firstResult.Value, Is.EqualTo(4)); + Assert.That(firstBuffer, Is.EqualTo(new byte[] { 0x11, 0x22, 0x33, 0x44 })); + + Assert.That(secondResult.IsSuccess); + Assert.That(secondResult.Value, Is.EqualTo(4)); + Assert.That(secondBuffer, Is.EqualTo(new byte[] { 0x55, 0x66, 0x77, 0x88 })); + } + + /// + /// Tests with a zero pointer. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesPartialWithZeroPointerTest() + { + var result = TestProcessMemory!.ReadBytesPartial(UIntPtr.Zero, new byte[4], 4); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnZeroPointer))); + } + + /// + /// Tests with an unreadable address. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesPartialWithUnreadableAddressTest() + { + var result = TestProcessMemory!.ReadBytesPartial(UIntPtr.MaxValue, new byte[1], 1); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); + var systemError = ((ReadFailureOnSystemRead)result.Error).Details; + Assert.That(systemError, Is.Not.Null); + } + + /// + /// Tests with a bad pointer path. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesPartialWithInvalidPathTest() + { + var result = TestProcessMemory!.ReadBytesPartial($"{OuterClassPointer:X}+10,10,0,0", new byte[4], 4); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); + var pathError = ((ReadFailureOnPointerPathEvaluation)result.Error).Details; + Assert.That(pathError, Is.Not.Null); + } + + /// + /// Tests with a zero length. + /// Expect the result to be an empty array of bytes. + /// + [Test] + public void ReadBytesPartialWithZeroLengthTest() + { + var result = TestProcessMemory!.ReadBytesPartial(OuterClassPointer, Array.Empty(), 0); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.Zero); + } + + /// + /// Tests with a length exceeding the buffer + /// capacity. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesPartialWithExcessiveLengthTest() + { + var result = TestProcessMemory!.ReadBytesPartial(OuterClassPointer, new byte[4], 8); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnInvalidArguments))); + } + + /// + /// Tests with an address and length that would + /// make the read start on a readable address but end on an unreadable one. + /// Expect the result to be 4 (as in 4 bytes read) and the buffer to contain the 4 bytes that were readable. + /// + [Test] + public void ReadBytesPartialOnPartiallyUnreadableRangeTest() + { + // Prepare a segment of memory that is isolated from other memory regions, and has a known sequence of bytes + // at the end. + var bytesAtTheEnd = new byte[] { 0x1, 0x2, 0x3, 0x4 }; + var allocatedMemory = TestProcessMemory!.Allocate(0x1000, false).Value; + var targetAddress = allocatedMemory.Range.End - 4; + var writeResult = TestProcessMemory.WriteBytes(targetAddress, bytesAtTheEnd, MemoryProtectionStrategy.Ignore); + Assert.That(writeResult.IsSuccess, Is.True); + + // Read 8 bytes, which should result in reading 4 bytes from the readable region and 4 bytes from the unreadable + // one. + var buffer = new byte[8]; + var result = TestProcessMemory.ReadBytesPartial(targetAddress, buffer, 8); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo(4)); + Assert.That(buffer, Is.EqualTo(new byte[] { 0x1, 0x2, 0x3, 0x4, 0, 0, 0, 0 })); + } + + #endregion + + #endregion + #region Primitive type reading /// @@ -182,22 +408,6 @@ public void ReadDoubleTest() ProceedToNextStep(); Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(-99879416311.4478)); } - - /// - /// Tests . - /// Reads a known byte array from the target process before and after the process changes their value. - /// It should be equal to its known values before and after modification. - /// - [Test] - public void ReadBytesTest() - { - PointerPath pointerPath = $"{OuterClassPointer:X}+18,10"; - Assert.That(TestProcessMemory!.ReadBytes(pointerPath, 4).GetValueOrDefault(), - Is.EqualTo(new byte[] { 0x11, 0x22, 0x33, 0x44 })); - ProceedToNextStep(); - Assert.That(TestProcessMemory.ReadBytes(pointerPath, 4).GetValueOrDefault(), - Is.EqualTo(new byte[] { 0x55, 0x66, 0x77, 0x88 })); - } /// /// Tests . @@ -333,31 +543,36 @@ public void ReadStructureWithReferencesTest() #region String reading - #region FindStringSettings + /// Settings expected for our target .net process. + private static readonly StringSettings ExpectedDotNetStringSettings = new(Encoding.Unicode, true, + new StringLengthPrefix(4, StringLengthUnit.Characters), new byte[8]); - public record FindStringSettingsTestCase(Encoding Encoding, string String, bool IsNullTerminated, StringLengthPrefix? LengthPrefix, - byte[]? TypePrefix); + /// Test case for tests using string settings. + public record StringSettingsTestCase(Encoding Encoding, string String, bool IsNullTerminated, + StringLengthPrefix? LengthPrefix, byte[]? TypePrefix); private static readonly byte[] ExampleTypePrefix = { 0x11, 0x22, 0x33, 0x44 }; - private static readonly FindStringSettingsTestCase[] FindStringSettingsTestCases = { + private static readonly StringSettingsTestCase[] StringSettingsTestCases = { new(Encoding.Unicode, "Simple string", true, null, null), new(Encoding.UTF8, "Simple string", true, null, null), new(Encoding.UTF8, "String with diàcrîtìçs", true, null, null), new(Encoding.Latin1, "String with diàcrîtìçs", true, null, null), new(Encoding.Latin1, "é", true, null, null), new(Encoding.Unicode, "Mµl十ÿहि鬱", true, null, null), - new(Encoding.Unicode, "Mµl十ÿहि鬱", true, new StringLengthPrefix(4, StringLengthPrefixUnit.Bytes), null), - new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(4, StringLengthPrefixUnit.Bytes), null), - new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(4, StringLengthPrefixUnit.Characters), null), - new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(2, StringLengthPrefixUnit.Characters), null), - new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(1, StringLengthPrefixUnit.Characters), null), - new(Encoding.UTF8, "Mµl十ÿहि鬱", false, new StringLengthPrefix(4, StringLengthPrefixUnit.Bytes), null), - new(Encoding.UTF8, "Mµl十ÿहि鬱", false, new StringLengthPrefix(4, StringLengthPrefixUnit.Characters), null), - new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(4, StringLengthPrefixUnit.Bytes), ExampleTypePrefix), + new(Encoding.Unicode, "Mµl十ÿहि鬱", true, new StringLengthPrefix(4, StringLengthUnit.Bytes), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(4, StringLengthUnit.Bytes), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(4, StringLengthUnit.Characters), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(2, StringLengthUnit.Characters), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(1, StringLengthUnit.Characters), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", false, new StringLengthPrefix(4, StringLengthUnit.Bytes), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", false, new StringLengthPrefix(4, StringLengthUnit.Characters), null), + new(Encoding.UTF8, "Mµl十ÿहि鬱", true, new StringLengthPrefix(4, StringLengthUnit.Bytes), ExampleTypePrefix), new(Encoding.UTF8, "Mµl十ÿहि鬱", true, null, ExampleTypePrefix), }; + #region FindStringSettings + /// /// Tests using predefined test cases. /// The methodology is to allocate some memory, write a string to it using the parameters defined in the test case, @@ -366,8 +581,8 @@ public record FindStringSettingsTestCase(Encoding Encoding, string String, bool /// /// This test depends on allocation methods and writing methods. If all test cases fail, check the tests /// for these features first. - [TestCaseSource(nameof(FindStringSettingsTestCases))] - public void FindStringSettingsTest(FindStringSettingsTestCase testCase) + [TestCaseSource(nameof(StringSettingsTestCases))] + public void FindStringSettingsTest(StringSettingsTestCase testCase) { var settings = new StringSettings(testCase.Encoding) { @@ -401,14 +616,10 @@ public void FindStringSettingsTest(FindStringSettingsTestCase testCase) { Assert.That(foundSettings.Encoding, Is.EqualTo(testCase.Encoding)); Assert.That(foundSettings.IsNullTerminated, Is.EqualTo(testCase.IsNullTerminated)); - Assert.That(foundSettings.LengthPrefix?.PrefixSize, Is.EqualTo(testCase.LengthPrefix?.PrefixSize)); + Assert.That(foundSettings.LengthPrefix?.Size, Is.EqualTo(testCase.LengthPrefix?.Size)); Assert.That(foundSettings.TypePrefix, Is.EqualTo(testCase.TypePrefix)); }); } - - /// Settings expected for our target .net process. - private static readonly StringSettings ExpectedDotNetStringSettings = new(Encoding.Unicode, true, - new StringLengthPrefix(4, StringLengthPrefixUnit.Characters), new byte[8]); /// /// Tests on a known string in the target @@ -422,8 +633,8 @@ public void FindStringSettingsOnKnownPathTest() Assert.That(result.IsSuccess, Is.True); Assert.That(result.Value.Encoding, Is.EqualTo(ExpectedDotNetStringSettings.Encoding)); Assert.That(result.Value.IsNullTerminated, Is.EqualTo(ExpectedDotNetStringSettings.IsNullTerminated)); - Assert.That(result.Value.LengthPrefix?.PrefixSize, - Is.EqualTo(ExpectedDotNetStringSettings.LengthPrefix?.PrefixSize)); + Assert.That(result.Value.LengthPrefix?.Size, + Is.EqualTo(ExpectedDotNetStringSettings.LengthPrefix?.Size)); Assert.That(result.Value.LengthPrefix?.Unit, Is.EqualTo(ExpectedDotNetStringSettings.LengthPrefix?.Unit)); // For the type prefix, we only check the length, because the actual value is dynamic. Assert.That(result.Value.TypePrefix?.Length, Is.EqualTo(ExpectedDotNetStringSettings.TypePrefix?.Length)); @@ -522,149 +733,392 @@ public void FindStringSettingsOnInvalidPointerPathTest() #endregion #region ReadRawString + + /// Test case for . + public record ReadRawStringTestCase(Encoding Encoding, string String, bool IsNullTerminated, int MaxLength, + string? ExpectedStringIfDifferent = null); + + private static readonly ReadRawStringTestCase[] ReadRawStringTestCases = { + new(Encoding.Unicode, "Simple string", true, 100), + new(Encoding.Unicode, "Simple string", true, 0, string.Empty), + new(Encoding.Unicode, "Mµl十ÿहि鬱", true, 10), + new(Encoding.Unicode, "Mµl十ÿहि鬱", false, 10, "Mµl十ÿहि鬱\0\0"), + new(Encoding.Unicode, "Mµl十ÿहि鬱", true, 4, "Mµl十"), + new(Encoding.Unicode, "Mµl十ÿहि鬱", false, 4, "Mµl十"), + new(Encoding.UTF8, "Simple string", true, 100), + new(Encoding.UTF8, "String with diàcrîtìçs", true, 100), + new(Encoding.Latin1, "String with diàcrîtìçs", true, 100), + }; + + /// + /// Tests . + /// Writes a string to memory using a specific encoding, then reads it back with the tested method using the same + /// encoding and other various parameters. + /// The result must match the expectation of the test case. + /// + /// Test case defining the parameters to test. + [TestCaseSource(nameof(ReadRawStringTestCases))] + public void ReadRawStringTest(ReadRawStringTestCase testCase) + { + var reservedMemory = TestProcessMemory!.Allocate(0x1000, false).Value.ReserveRange(0x1000).Value; + byte[] bytes = testCase.Encoding.GetBytes(testCase.String); + TestProcessMemory.Write(reservedMemory.Address, bytes); + + var result = TestProcessMemory.ReadRawString(reservedMemory.Address, testCase.Encoding, + testCase.MaxLength, testCase.IsNullTerminated); + Assert.That(result.IsSuccess, Is.True); + var resultString = result.Value; + + var expectedString = testCase.ExpectedStringIfDifferent ?? testCase.String; + Assert.That(resultString, Is.EqualTo(expectedString)); + } + + /// + /// Tests . + /// Calls the method with a pointer path that goes through a known string pointer, before and after the string + /// pointer is modified. + /// Expect the result to match the known strings in both cases. + /// + [Test] + public void ReadRawStringWithKnownStringTest() + { + var path = $"{OuterClassPointer:X}+8,C"; + var firstResult = TestProcessMemory!.ReadRawString(path, Encoding.Unicode); + ProceedToNextStep(); + var secondResult = TestProcessMemory.ReadRawString(path, Encoding.Unicode); + + Assert.That(firstResult.IsSuccess, Is.True); + Assert.That(firstResult.Value, Is.EqualTo("ThisIsÄString")); + + Assert.That(secondResult.IsSuccess, Is.True); + Assert.That(secondResult.Value, Is.EqualTo("ThisIsALongerStrîngWith文字化けチェック")); + } + + /// + /// Tests . + /// Call the method with a zero pointer as the address. + /// Expect the result to be a . + /// + [Test] + public void ReadRawStringWithZeroPointerTest() + { + var result = TestProcessMemory!.ReadRawString(UIntPtr.Zero, Encoding.Unicode); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnZeroPointer))); + } + + /// + /// Tests . + /// Call the method with an unreadable address. + /// Expect the result to be a . + /// + [Test] + public void ReadRawStringWithUnreadableAddressTest() + { + var result = TestProcessMemory!.ReadRawString(UIntPtr.MaxValue, Encoding.Unicode); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); + var systemError = ((ReadFailureOnSystemRead)result.Error).Details; + Assert.That(systemError, Is.Not.Null); + } + /// + /// Tests . + /// Call the method with a negative max length. + /// Expect the result to be a . + /// + [Test] + public void ReadRawStringWithNegativeMaxLengthTest() + { + var result = TestProcessMemory!.ReadRawString($"{OuterClassPointer:X}+8,C", Encoding.Unicode, -1); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnInvalidArguments))); + } + /// + /// Tests . + /// Call the method with a known path that cannot be evaluated. + /// Expect the result to be a . + /// + [Test] + public void ReadRawStringWithBadPointerPathTest() + { + var result = TestProcessMemory!.ReadRawString($"{OuterClassPointer:X}+10,10,0,0", Encoding.Unicode); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); + var pathError = ((ReadFailureOnPointerPathEvaluation)result.Error).Details; + Assert.That(pathError, Is.Not.Null); + } #endregion #region ReadStringPointer + /// + /// Tests using predefined test cases. + /// The methodology is to allocate some memory, write a string to it using the parameters defined in the test case, + /// and then call the tested method on it. + /// The result is expected to match the test case settings. + /// + /// This test depends on allocation methods and writing methods. If all test cases fail, check the tests + /// for these features first. + [TestCaseSource(nameof(StringSettingsTestCases))] + public void ReadStringPointerTest(StringSettingsTestCase testCase) + { + var settings = new StringSettings(testCase.Encoding) + { + IsNullTerminated = testCase.IsNullTerminated, + LengthPrefix = testCase.LengthPrefix, + TypePrefix = testCase.TypePrefix + }; + + // Allocate some memory to write the string. + var allocatedSpace = TestProcessMemory!.Allocate(1024, false).Value.ReserveRange(1024).Value; + byte[] bytes = settings.GetBytes(testCase.String) + ?? throw new ArgumentException("The test case is invalid: the length prefix is too short for the string."); + + // Fill the allocated space with FF bytes to prevent unexpected null termination results + TestProcessMemory.WriteBytes(allocatedSpace.Address, Enumerable.Repeat((byte)255, 1024).ToArray()); + + // Write the string to the allocated space. Because ReadStringPointer expects the address of a pointer to the + // string, we write the string with an offset of 8, and then we write the first 8 bytes to point to the address + // where we wrote the string. + TestProcessMemory.Write(allocatedSpace.Address + 8, bytes); + TestProcessMemory.Write(allocatedSpace.Address, allocatedSpace.Address + 8); + + // Call the tested method on the string pointer (the one we wrote last) and check that we got the same string + // that we wrote previously. + var result = TestProcessMemory.ReadStringPointer(allocatedSpace.Address, settings); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.EqualTo(testCase.String)); + } + + /// + /// Tests the method with a pointer path + /// that points to a known string in the target process. + /// The method is called before and after the string pointer is modified. + /// The result is expected to match the known strings in both cases. + /// + [Test] + public void ReadStringPointerOnKnownStringTest() + { + var path = $"{OuterClassPointer:X}+8"; + var firstResult = TestProcessMemory!.ReadStringPointer(path, ExpectedDotNetStringSettings); + ProceedToNextStep(); + var secondResult = TestProcessMemory.ReadStringPointer(path, ExpectedDotNetStringSettings); + + Assert.That(firstResult.IsSuccess, Is.True); + Assert.That(firstResult.Value, Is.EqualTo("ThisIsÄString")); + + Assert.That(secondResult.IsSuccess, Is.True); + Assert.That(secondResult.Value, Is.EqualTo("ThisIsALongerStrîngWith文字化けチェック")); + } + + /// + /// Tests with a zero pointer address. + /// Expect the result to be a . + /// + [Test] + public void ReadStringPointerWithZeroPointerTest() + { + var result = TestProcessMemory!.ReadStringPointer(UIntPtr.Zero, ExpectedDotNetStringSettings); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnZeroPointer))); + } + + /// + /// Tests with a pointer that reads 0. + /// Expect the result to be a . + /// + [Test] + public void ReadStringPointerWithPointerToZeroPointerTest() + { + // Arrange a space in memory that points to 0. + var allocatedSpace = TestProcessMemory!.Allocate(8, false).Value.ReserveRange(8).Value; + + var result = TestProcessMemory!.ReadStringPointer(allocatedSpace.Address, ExpectedDotNetStringSettings); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnZeroPointer))); + } + /// + /// Tests with a zero pointer address. + /// Expect the result to be a . + /// + [Test] + public void ReadStringPointerWithUnreadablePointerTest() + { + var result = TestProcessMemory!.ReadStringPointer(UIntPtr.MaxValue, ExpectedDotNetStringSettings); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnPointerReadFailure))); + var failure = (StringReadFailureOnPointerReadFailure)result.Error; + Assert.That(failure.Details, Is.Not.Null); + } - #endregion + /// + /// Tests with an address that points to + /// an unreadable memory region. + /// Expect the result to be a . + /// + [Test] + public void ReadStringPointerWithPointerToUnreadableMemoryTest() + { + var result = TestProcessMemory!.ReadStringPointer($"{OuterClassPointer:X}+10,10", + ExpectedDotNetStringSettings); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnStringBytesReadFailure))); + var failure = (StringReadFailureOnStringBytesReadFailure)result.Error; + Assert.That(failure.Details, Is.Not.Null); + } + + /// + /// Tests with invalid string settings. + /// Expect the result to be a . + /// + [Test] + public void ReadStringPointerWithInvalidSettingsTest() + { + var settings = new StringSettings(Encoding.Unicode, + isNullTerminated: false, + lengthPrefix: null); + var result = TestProcessMemory!.ReadStringPointer(OuterClassPointer+8, settings); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnInvalidSettings))); + } + + /// + /// Tests with a bad pointer path. + /// Expect the result to be a . + /// + [Test] + public void ReadStringPointerWithBadPointerPathTest() + { + var result = TestProcessMemory!.ReadStringPointer($"{OuterClassPointer:X}+10,10,0,0", + ExpectedDotNetStringSettings); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnPointerPathEvaluation))); + var failure = (StringReadFailureOnPointerPathEvaluation)result.Error; + Assert.That(failure.Details, Is.Not.Null); + } - // /// - // /// Tests . - // /// Reads a known string from the target process after initialization, without using any parameter beyond the - // /// pointer path. - // /// This method uses a to read the string at the right position. - // /// It should be equal to its known values before and after modification. - // /// - // [Test] - // public void ReadStringWithNoParametersTest() - // { - // var pointerPath = new PointerPath($"{OuterClassPointer:X}+8,8"); - // Assert.That(TestProcessMemory!.ReadString(pointerPath).GetValueOrDefault(), Is.EqualTo("ThisIsÄString")); - // ProceedToNextStep(); - // Assert.That(TestProcessMemory!.ReadString(pointerPath).GetValueOrDefault(), - // Is.EqualTo("ThisIsALongerStrîngWith文字化けチェック")); - // } - // - // /// - // /// Tests . - // /// Reads a known string from the target process after initialization, with a max length of 10 bytes. - // /// This method uses a to read the string at the right position. - // /// It should be equal to the first 5 characters of its known value, because in memory, the string is stored as in - // /// UTF-16, so it uses 2 bytes per ASCII-friendly character, so 10 bytes would be 5 characters. - // /// Despite the length prefix being read correctly as more than 10, only the 10 first bytes should be read. - // /// - // [Test] - // public void ReadStringWithLimitedLengthTest() - // => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", 10).GetValueOrDefault(), - // Is.EqualTo("ThisI")); - // - // /// - // /// Tests . - // /// Reads a known string from the target process after initialization, with a StringSettings instance similar to the - // /// .net preset but with no length prefix. - // /// This method uses a to read the string at the right position, after its length prefix. - // /// It should be equal to its full known value. Despite not being able to know the length of the string because we - // /// specify that there is no length prefix, we still use a setting that specifies a null terminator, and the string - // /// is indeed null-terminated, so it should properly cut after the last character. - // /// - // [Test] - // public void ReadStringWithoutLengthPrefixTest() - // => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,C", - // stringSettings: new StringSettings(Encoding.Unicode, true, null)).GetValueOrDefault(), - // Is.EqualTo("ThisIsÄString")); - // - // /// - // /// Tests . - // /// Reads a known string from the target process after initialization, with a StringSettings instance similar to the - // /// .net preset but not null-terminated. - // /// This method uses a to read the string at the right position. - // /// It should be equal to its full known value. Despite not being able to identify a null terminator as the end of - // /// the string, we do use a setting that specifies a correct length prefix, so only the right number of bytes should - // /// be read. - // /// - // [Test] - // public void ReadStringWithLengthPrefixWithoutNullTerminatorTest() - // => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", - // stringSettings: new StringSettings(Encoding.Unicode, false, new StringLengthPrefix(4, 2))) - // .GetValueOrDefault(), - // Is.EqualTo("ThisIsÄString")); - // - // /// - // /// Tests . - // /// Reads a known string from the target process after initialization, with a StringSettings instance specifying no - // /// null terminator and no length prefix. We use a byte count of 64. - // /// This method uses a to read the string at the right position, after the length prefix. - // /// It should be equal to its known value, followed by a bunch of garbage characters. Because we specified no length - // /// prefix and no null terminator, all 64 characters will be read as a string, despite the actual string being - // /// shorter than that. - // /// To clarify, even though the result looks wrong, this is the expected output. The input parameters are wrong. - // /// - // [Test] - // public void ReadStringWithoutLengthPrefixOrNullTerminatorTest() - // { - // string? result = TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,C", 64, - // new StringSettings(Encoding.Unicode, false, null)).GetValueOrDefault(); - // - // // We should have a string that starts with the full known string, and has at least one more character. - // // We cannot test an exact string or length because the memory region after the string is not guaranteed to - // // always be the same. - // Assert.That(result, Does.StartWith("ThisIsÄString")); - // Assert.That(result, Has.Length.AtLeast("ThisIsÄString".Length + 1)); - // } - // - // /// - // /// Tests . - // /// Reads a known string from the target process after initialization, with a StringSettings instance that has a - // /// length prefix unit set to null instead of 2, and no null terminator. - // /// This method uses a to read the string at the right position. - // /// It should be equal to its full known value. The unit being set to null should trigger it to automatically - // /// determine what the unit should be. It should determine that it is 2 and read the string correctly. - // /// - // [Test] - // public void ReadStringWithUnspecifiedLengthPrefixUnitTest() - // => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", - // stringSettings: new StringSettings(Encoding.Unicode, false, new StringLengthPrefix(4))) - // .GetValueOrDefault(), - // Is.EqualTo("ThisIsÄString")); - // - // /// - // /// Tests . - // /// Reads a known string from the target process after initialization, with a StringSettings instance that has a - // /// length prefix unit with a size of 2 instead of the correct 4. - // /// This method uses a to read the string at the right position, 2 bytes into the length - // /// prefix. - // /// The result should be an empty string, because the length prefix should be read as 0. - // /// - // [Test] - // public void ReadStringWithZeroLengthPrefixUnitTest() - // => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,A", - // stringSettings: new StringSettings(Encoding.Unicode, true, new StringLengthPrefix(2, 2))) - // .GetValueOrDefault(), - // Is.EqualTo(string.Empty)); - // - // /// - // /// Tests . - // /// Reads a known string from the target process after initialization, with a StringSettings instance that has a - // /// UTF-8 encoding instead of the correct UTF-16. - // /// This method uses a to read the string at the right position. - // /// The result should be only the first character of the know value, because the null terminator is hit on the - // /// second UTF-16 byte that is supposed to be part of the first character. - // /// To explain a little bit more, a UTF-16 string has a minimum of 2 bytes per character. In this case, the first - // /// two characters are "Th", which are held in memory as 54 00 68 00. But UTF-8 would write the same "Th" as 54 68. - // /// The encoding will interpret the second byte (00) as a null terminator that signals the end of the string, and - // /// read it only as "T" (54), discarding everything after that. - // /// - // [Test] - // public void ReadStringWithWrongEncodingTest() - // => Assert.That(TestProcessMemory!.ReadString($"{OuterClassPointer:X}+8,8", - // stringSettings: new StringSettings(Encoding.UTF8, true, new StringLengthPrefix(4, 2))) - // .GetValueOrDefault(), - // Is.EqualTo("T")); + /// + /// Tests . + /// The settings specify a length prefix in bytes. The pointer points to a string with a length prefix in bytes that + /// has a value of 10. + /// Performs 2 reads: one with a max length of 10 bytes, and one with a max length of 9 bytes. + /// Expect the first read to return the full string, and the second read to fail with a + /// . + /// + [Test] + public void ReadStringPointerWithTooLongStringWithBytesPrefixTest() + { + var allocatedSpace = TestProcessMemory!.Allocate(0x1000, false).Value.ReserveRange(0x1000).Value; + var settings = new StringSettings(Encoding.UTF8, false, new StringLengthPrefix(4, StringLengthUnit.Bytes)); + var stringContent = "0123456789"; // Length of 10 characters, and 10 bytes in UTF-8. + var bytes = settings.GetBytes(stringContent)!; + + // Write the string to the allocated space. Because ReadStringPointer expects the address of a pointer to the + // string, we write the string with an offset of 8, and then we write the first 8 bytes to point to the address + // where we wrote the string. + TestProcessMemory.Write(allocatedSpace.Address + 8, bytes); + TestProcessMemory.Write(allocatedSpace.Address, allocatedSpace.Address + 8); + + // Read the string with a max length of 10 bytes. + settings.MaxLength = 10; + var firstResult = TestProcessMemory!.ReadStringPointer(allocatedSpace.Address, settings); + + // Read the string with a max length of 9 bytes. + settings.MaxLength = 9; + var secondResult = TestProcessMemory.ReadStringPointer(allocatedSpace.Address, settings); + + Assert.That(firstResult.IsSuccess, Is.True); + Assert.That(firstResult.Value, Is.EqualTo(stringContent)); + + Assert.That(secondResult.IsSuccess, Is.False); + Assert.That(secondResult.Error, Is.TypeOf(typeof(StringReadFailureOnStringTooLong))); + var failure = (StringReadFailureOnStringTooLong)secondResult.Error; + Assert.That(failure.LengthPrefixValue, Is.EqualTo(10)); + } + + /// + /// Tests . + /// The settings specify a length prefix in bytes. The pointer points to a string with a length prefix in characters + /// that has a value of 10. + /// Performs 2 reads: one with a max length of 10 characters, and one with a max length of 9 characters. + /// Expect the first read to return the full string, and the second read to fail with a + /// . + /// + [Test] + public void ReadStringPointerWithTooLongStringWithCharactersPrefixTest() + { + var allocatedSpace = TestProcessMemory!.Allocate(0x1000, false).Value.ReserveRange(0x1000).Value; + var settings = new StringSettings(Encoding.Unicode, false, + new StringLengthPrefix(4, StringLengthUnit.Characters)); + var stringContent = "0123456789"; // Length of 10 characters, but 16 bytes in UTF-16. + var bytes = settings.GetBytes(stringContent)!; + + // Write the string to the allocated space. Because ReadStringPointer expects the address of a pointer to the + // string, we write the string with an offset of 8, and then we write the first 8 bytes to point to the address + // where we wrote the string. + TestProcessMemory.Write(allocatedSpace.Address + 8, bytes); + TestProcessMemory.Write(allocatedSpace.Address, allocatedSpace.Address + 8); + + // Read the string with a max length of 10 characters. + settings.MaxLength = 10; + var firstResult = TestProcessMemory!.ReadStringPointer(allocatedSpace.Address, settings); + + // Read the string with a max length of 9 characters. + settings.MaxLength = 9; + var secondResult = TestProcessMemory.ReadStringPointer(allocatedSpace.Address, settings); + + Assert.That(firstResult.IsSuccess, Is.True); + Assert.That(firstResult.Value, Is.EqualTo(stringContent)); + + Assert.That(secondResult.IsSuccess, Is.False); + Assert.That(secondResult.Error, Is.TypeOf(typeof(StringReadFailureOnStringTooLong))); + var failure = (StringReadFailureOnStringTooLong)secondResult.Error; + Assert.That(failure.LengthPrefixValue, Is.EqualTo(10)); + } + + /// + /// Tests . + /// The settings specify no length prefix, but a null terminator. The pointer points to a string with 10 characters. + /// Performs 2 reads: one with a max length of 10 characters, and one with a max length of 9 characters. + /// Expect the first read to return the full string, and the second read to fail with a + /// . + /// + [Test] + public void ReadStringPointerWithTooLongStringWithoutPrefixTest() + { + var allocatedSpace = TestProcessMemory!.Allocate(0x1000, false).Value.ReserveRange(0x1000).Value; + var settings = new StringSettings(Encoding.Unicode, true, null); + var stringContent = "0123456789"; // Length of 10 characters + var bytes = settings.GetBytes(stringContent)!; + + // Write the string to the allocated space. Because ReadStringPointer expects the address of a pointer to the + // string, we write the string with an offset of 8, and then we write the first 8 bytes to point to the address + // where we wrote the string. + TestProcessMemory.Write(allocatedSpace.Address + 8, bytes); + TestProcessMemory.Write(allocatedSpace.Address, allocatedSpace.Address + 8); + + // Read the string with a max length of 10 characters. + settings.MaxLength = 10; + var firstResult = TestProcessMemory!.ReadStringPointer(allocatedSpace.Address, settings); + + // Read the string with a max length of 9 characters. + settings.MaxLength = 9; + var secondResult = TestProcessMemory.ReadStringPointer(allocatedSpace.Address, settings); + + Assert.That(firstResult.IsSuccess, Is.True); + Assert.That(firstResult.Value, Is.EqualTo(stringContent)); + + Assert.That(secondResult.IsSuccess, Is.False); + Assert.That(secondResult.Error, Is.TypeOf(typeof(StringReadFailureOnStringTooLong))); + var failure = (StringReadFailureOnStringTooLong)secondResult.Error; + Assert.That(failure.LengthPrefixValue, Is.Null); // No prefix, so the value should be null. + } + + #endregion #endregion } \ No newline at end of file From 2f47badd6d0d16c9227c11cfcf411f8d7b09742c Mon Sep 17 00:00:00 2001 From: Doublevil Date: Thu, 6 Jun 2024 15:38:01 +0200 Subject: [PATCH 16/66] Removed obsolete primitive type write signatures --- .../ProcessMemory/ProcessMemory.Write.cs | 177 ------------------ 1 file changed, 177 deletions(-) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs index 8281943..41b5cec 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs @@ -58,23 +58,6 @@ public Result Write(UIntPtr address, T value, _ => WriteBytes(address, value.ToBytes(), memoryProtectionStrategy) }; } - - /// - /// Writes a boolean value to the address referred by the given pointer path in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Value to write. True will be written as a byte with the value 1. False will be written - /// as a byte with the value 0. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - private Result WriteBool(PointerPath path, bool value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - var addressResult = EvaluateMemoryAddress(path); - return addressResult.IsSuccess ? WriteBool(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); - } /// /// Writes a boolean value to the given address in the process memory. @@ -88,22 +71,6 @@ private Result WriteBool(PointerPath path, bool value, private Result WriteBool(UIntPtr address, bool value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, new[] { (byte)(value ? 1 : 0) }, memoryProtectionStrategy); - - /// - /// Writes a byte to the address referred by the given pointer path in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Value to write. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - private Result WriteByte(PointerPath path, byte value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - var addressResult = EvaluateMemoryAddress(path); - return addressResult.IsSuccess ? WriteByte(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); - } /// /// Writes a byte to the given address in the process memory. @@ -116,22 +83,6 @@ private Result WriteByte(PointerPath path, byte value, private Result WriteByte(UIntPtr address, byte value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, new[] { value }, memoryProtectionStrategy); - - /// - /// Writes a short to the address referred by the given pointer path in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Value to write. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - private Result WriteShort(PointerPath path, short value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - var addressResult = EvaluateMemoryAddress(path); - return addressResult.IsSuccess ? WriteShort(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); - } /// /// Writes a short to the given address in the process memory. @@ -144,22 +95,6 @@ private Result WriteShort(PointerPath path, short value, private Result WriteShort(UIntPtr address, short value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - - /// - /// Writes an unsigned short to the address referred by the given pointer path in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Value to write. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - private Result WriteUShort(PointerPath path, ushort value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - var addressResult = EvaluateMemoryAddress(path); - return addressResult.IsSuccess ? WriteUShort(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); - } /// /// Writes an unsigned short to the given address in the process memory. @@ -172,22 +107,6 @@ private Result WriteUShort(PointerPath path, ushort value, private Result WriteUShort(UIntPtr address, ushort value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - - /// - /// Writes an integer to the address referred by the given pointer path in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Value to write. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - private Result WriteInt(PointerPath path, int value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - var addressResult = EvaluateMemoryAddress(path); - return addressResult.IsSuccess ? WriteInt(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); - } /// /// Writes an integer to the given address in the process memory. @@ -200,22 +119,6 @@ private Result WriteInt(PointerPath path, int value, private Result WriteInt(UIntPtr address, int value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - - /// - /// Writes an unsigned integer to the address referred by the given pointer path in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Value to write. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - private Result WriteUInt(PointerPath path, uint value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - var addressResult = EvaluateMemoryAddress(path); - return addressResult.IsSuccess ? WriteUInt(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); - } /// /// Writes an unsigned integer to the given address in the process memory. @@ -228,22 +131,6 @@ private Result WriteUInt(PointerPath path, uint value, private Result WriteUInt(UIntPtr address, uint value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - - /// - /// Writes a pointer to the address referred by the given pointer path in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Value to write. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - private Result WriteIntPtr(PointerPath path, IntPtr value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - var addressResult = EvaluateMemoryAddress(path); - return addressResult.IsSuccess ? WriteIntPtr(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); - } /// /// Writes a pointer to the given address in the process memory. @@ -256,22 +143,6 @@ private Result WriteIntPtr(PointerPath path, IntPtr value, private Result WriteIntPtr(UIntPtr address, IntPtr value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, value.ToBytes(_is64Bits), memoryProtectionStrategy); - - /// - /// Writes a float to the address referred by the given pointer path in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Value to write. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - private Result WriteFloat(PointerPath path, float value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - var addressResult = EvaluateMemoryAddress(path); - return addressResult.IsSuccess ? WriteFloat(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); - } /// /// Writes a float to the given address in the process memory. @@ -284,22 +155,6 @@ private Result WriteFloat(PointerPath path, float value, private Result WriteFloat(UIntPtr address, float value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - - /// - /// Writes a long to the address referred by the given pointer path in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Value to write. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - private Result WriteLong(PointerPath path, long value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - var addressResult = EvaluateMemoryAddress(path); - return addressResult.IsSuccess ? WriteLong(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); - } /// /// Writes a long to the given address in the process memory. @@ -312,22 +167,6 @@ private Result WriteLong(PointerPath path, long value, private Result WriteLong(UIntPtr address, long value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - - /// - /// Writes an unsigned long to the address referred by the given pointer path in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Value to write. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - private Result WriteULong(PointerPath path, ulong value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - var addressResult = EvaluateMemoryAddress(path); - return addressResult.IsSuccess ? WriteULong(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); - } /// /// Writes an unsigned long to the given address in the process memory. @@ -340,22 +179,6 @@ private Result WriteULong(PointerPath path, ulong value, private Result WriteULong(UIntPtr address, ulong value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - - /// - /// Writes a double to the address referred by the given pointer path in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Value to write. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - private Result WriteDouble(PointerPath path, double value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - var addressResult = EvaluateMemoryAddress(path); - return addressResult.IsSuccess ? WriteDouble(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); - } /// /// Writes a double to the given address in the process memory. From f1e49c8a9777b694ca61ef784e52e5831d114538 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Thu, 6 Jun 2024 22:09:02 +0200 Subject: [PATCH 17/66] Reorganized write methods to match read methods --- .../ProcessMemory/ProcessMemory.Write.cs | 126 ++++++++++-------- 1 file changed, 67 insertions(+), 59 deletions(-) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs index 41b5cec..5eca263 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs @@ -7,6 +7,72 @@ namespace MindControl; // This partial class implements the memory writing features of ProcessMemory. public partial class ProcessMemory { + #region Bytes writing + + /// + /// Writes a sequence of bytes to the address referred by the given pointer path in the process memory. + /// + /// Optimized, reusable path to the target address. + /// Value to write. + /// Strategy to use to deal with memory protection. If null (default), the + /// of this instance is used. + /// A successful result, or a write failure + public Result WriteBytes(PointerPath path, byte[] value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + var addressResult = EvaluateMemoryAddress(path); + return addressResult.IsSuccess ? WriteBytes(addressResult.Value, value, memoryProtectionStrategy) + : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + } + + /// + /// Writes a sequence of bytes to the given address in the process memory. + /// + /// Target address in the process memory. + /// Value to write. + /// Strategy to use to deal with memory protection. If null (default), the + /// of this instance is used. + /// A successful result, or a write failure + public Result WriteBytes(UIntPtr address, byte[] value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + // Remove protection if needed + memoryProtectionStrategy ??= DefaultWriteStrategy; + MemoryProtection? previousProtection = null; + if (memoryProtectionStrategy is MemoryProtectionStrategy.Remove or MemoryProtectionStrategy.RemoveAndRestore) + { + var protectionRemovalResult = _osService.ReadAndOverwriteProtection(_processHandle, _is64Bits, + address, MemoryProtection.ExecuteReadWrite); + + if (protectionRemovalResult.IsFailure) + return new WriteFailureOnSystemProtectionRemoval(address, protectionRemovalResult.Error); + + previousProtection = protectionRemovalResult.Value; + } + + // Write memory + var writeResult = _osService.WriteProcessMemory(_processHandle, address, value); + if (writeResult.IsFailure) + return new WriteFailureOnSystemWrite(address, writeResult.Error); + + // Restore protection if needed + if (memoryProtectionStrategy == MemoryProtectionStrategy.RemoveAndRestore + && previousProtection != MemoryProtection.ExecuteReadWrite) + { + var protectionRestorationResult = _osService.ReadAndOverwriteProtection(_processHandle, _is64Bits, + address, previousProtection!.Value); + + if (protectionRestorationResult.IsFailure) + return new WriteFailureOnSystemProtectionRestoration(address, protectionRestorationResult.Error); + } + + return Result.Success; + } + + #endregion + + #region Primitive types writing + /// /// Writes a value to the address referred by the given pointer path in the process memory. /// @@ -191,64 +257,6 @@ private Result WriteULong(UIntPtr address, ulong value, private Result WriteDouble(UIntPtr address, double value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); - - /// - /// Writes a sequence of bytes to the address referred by the given pointer path in the process memory. - /// - /// Optimized, reusable path to the target address. - /// Value to write. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - public Result WriteBytes(PointerPath path, byte[] value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - var addressResult = EvaluateMemoryAddress(path); - return addressResult.IsSuccess ? WriteBytes(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); - } - /// - /// Writes a sequence of bytes to the given address in the process memory. - /// - /// Target address in the process memory. - /// Value to write. - /// Strategy to use to deal with memory protection. If null (default), the - /// of this instance is used. - /// A successful result, or a write failure - public Result WriteBytes(UIntPtr address, byte[] value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) - { - // Remove protection if needed - memoryProtectionStrategy ??= DefaultWriteStrategy; - MemoryProtection? previousProtection = null; - if (memoryProtectionStrategy is MemoryProtectionStrategy.Remove or MemoryProtectionStrategy.RemoveAndRestore) - { - var protectionRemovalResult = _osService.ReadAndOverwriteProtection(_processHandle, _is64Bits, - address, MemoryProtection.ExecuteReadWrite); - - if (protectionRemovalResult.IsFailure) - return new WriteFailureOnSystemProtectionRemoval(address, protectionRemovalResult.Error); - - previousProtection = protectionRemovalResult.Value; - } - - // Write memory - var writeResult = _osService.WriteProcessMemory(_processHandle, address, value); - if (writeResult.IsFailure) - return new WriteFailureOnSystemWrite(address, writeResult.Error); - - // Restore protection if needed - if (memoryProtectionStrategy == MemoryProtectionStrategy.RemoveAndRestore - && previousProtection != MemoryProtection.ExecuteReadWrite) - { - var protectionRestorationResult = _osService.ReadAndOverwriteProtection(_processHandle, _is64Bits, - address, previousProtection!.Value); - - if (protectionRestorationResult.IsFailure) - return new WriteFailureOnSystemProtectionRestoration(address, protectionRestorationResult.Error); - } - - return Result.Success; - } + #endregion } \ No newline at end of file From 7bb26949b868667f33dda9e11e15064f319531d6 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Thu, 6 Jun 2024 23:02:38 +0200 Subject: [PATCH 18/66] StoreString implementation --- .../ProcessMemory/ProcessMemory.Allocation.cs | 50 +++++++- src/MindControl/Results/AllocationFailure.cs | 17 +++ src/MindControl/Results/ReservationFailure.cs | 17 +++ src/MindControl/StringSettings.cs | 18 ++- .../ProcessMemoryAllocationTest.cs | 119 +++++++++++++++++- .../ProcessMemoryReadTest.cs | 28 ++--- .../ProcessMemoryTests/ProcessMemoryTest.cs | 9 +- 7 files changed, 233 insertions(+), 25 deletions(-) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index 80d8a56..2d988f9 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -195,8 +195,8 @@ public Result Store(byte[] data, bool isCo /// /// Stores the given data in the specified allocated range. Returns the reservation that holds the data. /// In most situations, you should use the or signatures - /// instead, to have the instance handle allocations automatically, unless you need to - /// manage them manually. + /// instead, to have the instance handle allocations automatically. Use this signature + /// if you need to manage allocations and reservations manually. /// /// Data to store. /// Allocated memory to store the data. @@ -227,8 +227,8 @@ public Result Store(T value) /// Stores the given value or structure in the specified range of memory. Returns the reservation that holds the /// data. /// In most situations, you should use the or signatures - /// instead, to have the instance handle allocations automatically, unless you need to - /// manage them manually. + /// instead, to have the instance handle allocations automatically. Use this signature + /// if you need to manage allocations and reservations manually. /// /// Value or structure to store. /// Range of memory to store the data in. @@ -236,6 +236,48 @@ public Result Store(T value) /// The reservation holding the data. public Result Store(T value, MemoryAllocation allocation) where T: struct => Store(value.ToBytes(), allocation); + + /// + /// Stores the given string in the process memory. If needed, memory is allocated to store the string. + /// Returns the reservation that holds the string. + /// + /// String to store. + /// String settings to use to write the string. + /// The reservation holding the string. + public Result StoreString(string value, StringSettings settings) + { + if (!settings.IsValid) + return new AllocationFailureOnInvalidArguments(StringSettings.InvalidSettingsMessage); + + var bytes = settings.GetBytes(value); + if (bytes == null) + return new AllocationFailureOnInvalidArguments(StringSettings.GetBytesFailureMessage); + + return Store(bytes, isCode: false); + } + /// + /// Stores the given string in the specified range of memory. Returns the reservation that holds the string. + /// In most situations, you should use the signature instead, to + /// have the instance handle allocations automatically. Use this signature if you need + /// to manage allocations and reservations manually. + /// + /// String to store. + /// String settings to use to write the string. + /// Range of memory to store the string in. + /// The reservation holding the string. + public Result StoreString(string value, StringSettings settings, + MemoryAllocation allocation) + { + if (!settings.IsValid) + return new ReservationFailureOnInvalidArguments(StringSettings.InvalidSettingsMessage); + + var bytes = settings.GetBytes(value); + if (bytes == null) + return new ReservationFailureOnInvalidArguments(StringSettings.GetBytesFailureMessage); + + return Store(bytes, allocation); + } + #endregion } \ No newline at end of file diff --git a/src/MindControl/Results/AllocationFailure.cs b/src/MindControl/Results/AllocationFailure.cs index 81cd6d4..2306d20 100644 --- a/src/MindControl/Results/AllocationFailure.cs +++ b/src/MindControl/Results/AllocationFailure.cs @@ -5,6 +5,11 @@ /// public enum AllocationFailureReason { + /// + /// The arguments provided to the allocation operation are invalid. + /// + InvalidArguments, + /// /// The provided limit range is not within the bounds of the target process applicative memory range. /// @@ -23,6 +28,18 @@ public enum AllocationFailureReason /// Reason for the failure. public abstract record AllocationFailure(AllocationFailureReason Reason); +/// +/// Represents a failure in a memory allocation operation when the provided arguments are invalid. +/// +/// Message that describes how the arguments fail to meet expectations. +public record AllocationFailureOnInvalidArguments(string Message) + : AllocationFailure(AllocationFailureReason.InvalidArguments) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The arguments provided are invalid: {Message}"; +} + /// /// Represents a failure in a memory allocation operation when the provided limit range is not within the bounds of the /// target process applicative memory range. diff --git a/src/MindControl/Results/ReservationFailure.cs b/src/MindControl/Results/ReservationFailure.cs index a344e57..f0233b6 100644 --- a/src/MindControl/Results/ReservationFailure.cs +++ b/src/MindControl/Results/ReservationFailure.cs @@ -5,6 +5,11 @@ /// public enum ReservationFailureReason { + /// + /// The arguments provided to the reservation operation are invalid. + /// + InvalidArguments, + /// /// No space is available within the allocated memory range to reserve the specified size. /// @@ -17,6 +22,18 @@ public enum ReservationFailureReason /// Reason for the failure. public abstract record ReservationFailure(ReservationFailureReason Reason); +/// +/// Represents a failure in a memory reservation operation when the provided arguments are invalid. +/// +/// Message that describes how the arguments fail to meet expectations. +public record ReservationFailureOnInvalidArguments(string Message) + : ReservationFailure(ReservationFailureReason.InvalidArguments) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The arguments provided are invalid: {Message}"; +} + /// /// Represents a failure in a memory reservation operation when no space is available within the allocated memory range /// to reserve the specified size. diff --git a/src/MindControl/StringSettings.cs b/src/MindControl/StringSettings.cs index 5e1e093..1f78c6e 100644 --- a/src/MindControl/StringSettings.cs +++ b/src/MindControl/StringSettings.cs @@ -9,6 +9,18 @@ namespace MindControl; /// public class StringSettings { + /// + /// Failure message to be returned by string-related methods when the provided settings are invalid. + /// + internal const string InvalidSettingsMessage = + "The settings are not valid. Either a length prefix or null terminator is required."; + + /// + /// Failure message to be returned by string-related methods when returns a null value. + /// + internal const string GetBytesFailureMessage = + "The string settings are not valid for the specified string. The string might be too long for the length prefix to store it properly."; + /// /// Default value for . /// @@ -236,13 +248,15 @@ public int GetByteCount(string value) if (LengthPrefix is { Size: > 0, Unit: StringLengthUnit.Characters }) { ulong characterLength = bytes.Slice(typePrefixSize, lengthPrefixSize).ReadUnsignedNumber(); - if (IsNullTerminated) - characterLength++; if (characterLength == (ulong)resultingString.Length) return resultingString; if (characterLength > (ulong)resultingString.Length) return null; + + if (IsNullTerminated) + characterLength++; + resultingString = resultingString[..(int)characterLength]; } diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index 07586ab..589d5e7 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -1,4 +1,6 @@ -using NUnit.Framework; +using System.Text; +using MindControl.Results; +using NUnit.Framework; namespace MindControl.Test.ProcessMemoryTests; @@ -64,7 +66,7 @@ public void AllocateZeroTest() /// Stores a byte array in an allocated range and verifies that the value has been stored properly. /// [Test] - public void StoreWithRangeTest() + public void StoreWithAllocationTest() { var value = new byte[] { 1, 2, 3, 4 }; var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; @@ -157,4 +159,117 @@ public void StoreWithMultipleOverflowingValuesTest() Assert.That(secondStoreResult.IsSuccess, Is.True); Assert.That(TestProcessMemory!.Allocations, Has.Count.EqualTo(2)); } + + /// + /// Tests the method. + /// Stores a string with specific settings and read them back using the same settings. + /// When read back, the string should be the same as the original string. + /// + [Test] + public void StoreStringWithoutAllocationTest() + { + var stringToStore = "Hello 世界!"; + var reservationResult = TestProcessMemory!.StoreString(stringToStore, DotNetStringSettings); + Assert.That(reservationResult.IsSuccess, Is.True); + var reservation = reservationResult.Value; + + var bytesReadBack = TestProcessMemory.ReadBytes(reservation.Address, reservation.Range.GetSize()).Value; + var stringReadBack = DotNetStringSettings.GetString(bytesReadBack); + + // The store method should have allocated a new range. + Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); + Assert.That(reservation.IsDisposed, Is.False); + + // When we read back our string, we should get the same value we stored. + Assert.That(stringReadBack, Is.EqualTo(stringToStore)); + } + + /// + /// Tests the method. + /// Stores a string in a pre-allocated range with specific settings and read them back using the same settings. + /// When read back, the string should be the same as the original string. + /// + [Test] + public void StoreStringWithAllocationTest() + { + var stringToStore = "Hello 世界!"; + var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; + + var reservationResult = TestProcessMemory.StoreString(stringToStore, DotNetStringSettings, allocation); + Assert.That(reservationResult.IsSuccess, Is.True); + var reservation = reservationResult.Value; + + var bytesReadBack = TestProcessMemory.ReadBytes(reservation.Address, reservation.Range.GetSize()).Value; + var stringReadBack = DotNetStringSettings.GetString(bytesReadBack); + + Assert.That(reservation.IsDisposed, Is.False); + // The resulting range should be a range reserved from our original range. + Assert.That(reservation.ParentAllocation, Is.EqualTo(allocation)); + Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); + // When we read over the range, we should get the same value we stored. + Assert.That(stringReadBack, Is.EqualTo(stringToStore)); + } + + /// + /// Tests the method. + /// Specify invalid settings. The result is expected to be an . + /// + [Test] + public void StoreStringWithoutAllocationWithInvalidSettingsTest() + { + var invalidSettings = new StringSettings(Encoding.UTF8, isNullTerminated: false, lengthPrefix: null); + var result = TestProcessMemory!.StoreString("Hello world", invalidSettings); + + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// + /// Tests the method. + /// Specify valid settings, but with a length prefix that is too short to store the provided string. The result is + /// expected to be an . + /// + [Test] + public void StoreStringWithoutAllocationWithIncompatibleSettingsTest() + { + var settingsThatCanOnlyStoreUpTo255Chars = new StringSettings(Encoding.UTF8, isNullTerminated: false, + new StringLengthPrefix(1, StringLengthUnit.Characters)); + var result = TestProcessMemory!.StoreString(new string('a', 256), settingsThatCanOnlyStoreUpTo255Chars); + + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// + /// Tests the method. + /// Specify invalid settings. The result is expected to be an . + /// + [Test] + public void StoreStringWithAllocationWithInvalidSettingsTest() + { + var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; + var invalidSettings = new StringSettings(Encoding.UTF8, isNullTerminated: false, lengthPrefix: null); + var result = TestProcessMemory!.StoreString("Hello world", invalidSettings, allocation); + + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// + /// Tests the method. + /// Specify valid settings, but with a length prefix that is too short to store the provided string. The result is + /// expected to be an . + /// + [Test] + public void StoreStringWithAllocationWithIncompatibleSettingsTest() + { + var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; + var settingsThatCanOnlyStoreUpTo255Chars = new StringSettings(Encoding.UTF8, isNullTerminated: false, + new StringLengthPrefix(1, StringLengthUnit.Characters)); + var result = TestProcessMemory!.StoreString(new string('a', 256), settingsThatCanOnlyStoreUpTo255Chars, + allocation); + + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index 9de8ee8..20ba828 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -543,10 +543,6 @@ public void ReadStructureWithReferencesTest() #region String reading - /// Settings expected for our target .net process. - private static readonly StringSettings ExpectedDotNetStringSettings = new(Encoding.Unicode, true, - new StringLengthPrefix(4, StringLengthUnit.Characters), new byte[8]); - /// Test case for tests using string settings. public record StringSettingsTestCase(Encoding Encoding, string String, bool IsNullTerminated, StringLengthPrefix? LengthPrefix, byte[]? TypePrefix); @@ -631,13 +627,13 @@ public void FindStringSettingsOnKnownPathTest() var pointerPath = new PointerPath($"{OuterClassPointer:X}+8"); var result = TestProcessMemory!.FindStringSettings(pointerPath, "ThisIsÄString"); Assert.That(result.IsSuccess, Is.True); - Assert.That(result.Value.Encoding, Is.EqualTo(ExpectedDotNetStringSettings.Encoding)); - Assert.That(result.Value.IsNullTerminated, Is.EqualTo(ExpectedDotNetStringSettings.IsNullTerminated)); + Assert.That(result.Value.Encoding, Is.EqualTo(DotNetStringSettings.Encoding)); + Assert.That(result.Value.IsNullTerminated, Is.EqualTo(DotNetStringSettings.IsNullTerminated)); Assert.That(result.Value.LengthPrefix?.Size, - Is.EqualTo(ExpectedDotNetStringSettings.LengthPrefix?.Size)); - Assert.That(result.Value.LengthPrefix?.Unit, Is.EqualTo(ExpectedDotNetStringSettings.LengthPrefix?.Unit)); + Is.EqualTo(DotNetStringSettings.LengthPrefix?.Size)); + Assert.That(result.Value.LengthPrefix?.Unit, Is.EqualTo(DotNetStringSettings.LengthPrefix?.Unit)); // For the type prefix, we only check the length, because the actual value is dynamic. - Assert.That(result.Value.TypePrefix?.Length, Is.EqualTo(ExpectedDotNetStringSettings.TypePrefix?.Length)); + Assert.That(result.Value.TypePrefix?.Length, Is.EqualTo(DotNetStringSettings.TypePrefix?.Length)); } /// @@ -903,9 +899,9 @@ public void ReadStringPointerTest(StringSettingsTestCase testCase) public void ReadStringPointerOnKnownStringTest() { var path = $"{OuterClassPointer:X}+8"; - var firstResult = TestProcessMemory!.ReadStringPointer(path, ExpectedDotNetStringSettings); + var firstResult = TestProcessMemory!.ReadStringPointer(path, DotNetStringSettings); ProceedToNextStep(); - var secondResult = TestProcessMemory.ReadStringPointer(path, ExpectedDotNetStringSettings); + var secondResult = TestProcessMemory.ReadStringPointer(path, DotNetStringSettings); Assert.That(firstResult.IsSuccess, Is.True); Assert.That(firstResult.Value, Is.EqualTo("ThisIsÄString")); @@ -921,7 +917,7 @@ public void ReadStringPointerOnKnownStringTest() [Test] public void ReadStringPointerWithZeroPointerTest() { - var result = TestProcessMemory!.ReadStringPointer(UIntPtr.Zero, ExpectedDotNetStringSettings); + var result = TestProcessMemory!.ReadStringPointer(UIntPtr.Zero, DotNetStringSettings); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnZeroPointer))); } @@ -936,7 +932,7 @@ public void ReadStringPointerWithPointerToZeroPointerTest() // Arrange a space in memory that points to 0. var allocatedSpace = TestProcessMemory!.Allocate(8, false).Value.ReserveRange(8).Value; - var result = TestProcessMemory!.ReadStringPointer(allocatedSpace.Address, ExpectedDotNetStringSettings); + var result = TestProcessMemory!.ReadStringPointer(allocatedSpace.Address, DotNetStringSettings); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnZeroPointer))); } @@ -948,7 +944,7 @@ public void ReadStringPointerWithPointerToZeroPointerTest() [Test] public void ReadStringPointerWithUnreadablePointerTest() { - var result = TestProcessMemory!.ReadStringPointer(UIntPtr.MaxValue, ExpectedDotNetStringSettings); + var result = TestProcessMemory!.ReadStringPointer(UIntPtr.MaxValue, DotNetStringSettings); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnPointerReadFailure))); var failure = (StringReadFailureOnPointerReadFailure)result.Error; @@ -964,7 +960,7 @@ public void ReadStringPointerWithUnreadablePointerTest() public void ReadStringPointerWithPointerToUnreadableMemoryTest() { var result = TestProcessMemory!.ReadStringPointer($"{OuterClassPointer:X}+10,10", - ExpectedDotNetStringSettings); + DotNetStringSettings); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnStringBytesReadFailure))); var failure = (StringReadFailureOnStringBytesReadFailure)result.Error; @@ -994,7 +990,7 @@ public void ReadStringPointerWithInvalidSettingsTest() public void ReadStringPointerWithBadPointerPathTest() { var result = TestProcessMemory!.ReadStringPointer($"{OuterClassPointer:X}+10,10,0,0", - ExpectedDotNetStringSettings); + DotNetStringSettings); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnPointerPathEvaluation))); var failure = (StringReadFailureOnPointerPathEvaluation)result.Error; diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs index 07158b2..230f0f1 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs @@ -13,9 +13,16 @@ namespace MindControl.Test.ProcessMemoryTests; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ProcessMemoryTest { + /// Name of the main module of the target app. protected const string MainModuleName = "MindControl.Test.TargetApp.dll"; - protected Process? _targetProcess; + /// Settings that apply to strings used in our target .net process. + /// The type prefix is dynamic in reality. Here, we use a stub array with only 0, which is enough to serve + /// our purposes for the tests. + protected static readonly StringSettings DotNetStringSettings = new(Encoding.Unicode, true, + new StringLengthPrefix(4, StringLengthUnit.Characters), new byte[8]); + + private Process? _targetProcess; protected ProcessMemory? TestProcessMemory; protected UIntPtr OuterClassPointer; From e4f4f905fa6fdb566cc6141b7f09acd73d0660f7 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Thu, 6 Jun 2024 23:25:17 +0200 Subject: [PATCH 19/66] New test for string pointer (over)writing --- .../ProcessMemoryWriteTest.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs index c58c6e1..b30dbb0 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs @@ -98,4 +98,29 @@ public void WriteStructTest() Assert.That(readBackResult.IsSuccess, Is.True); Assert.That(readBackResult.Value, Is.EqualTo(structInstance)); } + + /// + /// This tests a combination of methods that can be used to overwrite a string pointer in the target app memory. + /// The protocol is: read the string settings from the target string pointer (because we know the initial value), + /// store the new string in memory, and then replace the pointer value with the address of the new string. + /// We then assert that the final output of the target app reflects the change. + /// + /// There is no shortcut method that does all of this at once, by design, to make sure users understand + /// that they are performing an allocation or reservation operation when they write a string. + [Test] + public void WriteStringPointerTest() + { + var pointerAddress = OuterClassPointer + 8; + var stringSettings = TestProcessMemory!.FindStringSettings(pointerAddress, "ThisIsÄString").Value; + ProceedToNextStep(); // Make sure we make the test program change the string pointer before we overwrite it + + var newString = "ThisStringIsCompletelyOriginalAndAlsoLongerThanTheOriginalOne"; + var newStringReservation = TestProcessMemory.StoreString(newString, stringSettings).Value; + TestProcessMemory.Write(pointerAddress, newStringReservation.Address); + + ProceedToNextStep(); // Makes the test program output the final results + + // Test that the program actually used (wrote to the console) the string that we hacked in + Assert.That(FinalResults[4], Is.EqualTo(newString)); + } } \ No newline at end of file From 07526e5d67a88e32b88f9243f66868a20587ce07 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Fri, 7 Jun 2024 18:55:58 +0200 Subject: [PATCH 20/66] Benchmarks & optimizations on string reading --- .../ProcessMemory/ProcessMemory.Read.cs | 55 +++++++++++++++--- .../{ => Benchmarks}/PointerPathBenchmark.cs | 2 +- .../ReadIntByAddressBenchmark.cs | 2 +- .../ReadLongByPointerPathBenchmark.cs | 2 +- .../Benchmarks/ReadRawStringBenchmark.cs | 52 +++++++++++++++++ .../Benchmarks/ReadStringPointerBenchmark.cs | 56 +++++++++++++++++++ .../WriteIntByAddressBenchmark.cs | 2 +- .../WriteLongByPointerPathBenchmark.cs | 2 +- 8 files changed, 159 insertions(+), 14 deletions(-) rename test/MindControl.Benchmark/{ => Benchmarks}/PointerPathBenchmark.cs (91%) rename test/MindControl.Benchmark/{ => Benchmarks}/ReadIntByAddressBenchmark.cs (97%) rename test/MindControl.Benchmark/{ => Benchmarks}/ReadLongByPointerPathBenchmark.cs (98%) create mode 100644 test/MindControl.Benchmark/Benchmarks/ReadRawStringBenchmark.cs create mode 100644 test/MindControl.Benchmark/Benchmarks/ReadStringPointerBenchmark.cs rename test/MindControl.Benchmark/{ => Benchmarks}/WriteIntByAddressBenchmark.cs (95%) rename test/MindControl.Benchmark/{ => Benchmarks}/WriteLongByPointerPathBenchmark.cs (97%) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs index 5635270..5612805 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs @@ -409,7 +409,35 @@ public Result FindStringSettings(UInt #endregion #region String reading + + /// + /// Maximum size of the stream buffer when reading strings. + /// Setting it too low may multiply the number of reads, which is inefficient. + /// Setting it too high will lead to very large, wasteful reads for bigger strings, and may also trigger reading + /// failures, with failure mitigation algorithms kicking in but taking resources that might not be needed. + /// + private const int StringReadingBufferMaxSize = 512; + /// + /// Size of the stream buffer when reading strings with an unknown length. + /// Setting it too low may multiply the number of reads, which is inefficient. + /// Setting it too high may lead to wasteful reads for smaller strings. + /// + /// + /// This setting is related to , the difference being that this one + /// measures the size in bytes, while the other measures the size in characters. + /// Setting this to 2 times the other allows for better performance with characters encoded on fewer bytes. + /// Setting this to 4 times the other allows for better performance with characters encoded on more bytes. + /// + private const int StringReadingBufferDefaultSize = 128; + + /// + /// Initial size of the string builder when reading characters of a string that has an unknown length. + /// Setting this value too low may lead to multiple resizes of the string builder, which is inefficient. + /// Setting this value too high may lead to unnecessary memory usage. + /// + private const int StringReadingBuilderDefaultSize = 32; + /// /// Reads a string from the address referred by the given pointer path, in the process memory. /// The address must point to the start of the actual string bytes. Consider @@ -618,23 +646,26 @@ private Result ReadStringWithLengthPrefixInCharacters // Get a memory stream after the prefixes int stringBytesOffset = lengthPrefixOffset + settings.LengthPrefix.Size; + int bufferSize = Math.Min(settings.Encoding.GetMaxByteCount((int)expectedStringLength), + StringReadingBufferMaxSize); using var stream = GetMemoryStream(address + stringBytesOffset); - using var streamReader = new StreamReader(stream, settings.Encoding); + using var streamReader = new StreamReader(stream, settings.Encoding, false, bufferSize, false); // Read characters until we reach the expected length - var stringBuilder = new StringBuilder((int)expectedStringLength); + // Using a char array here is a very small optimization over using a string builder + var characters = new char[expectedStringLength]; for (ulong i = 0; i < expectedStringLength; i++) { int nextChar = streamReader.Read(); // If we reach the end of the stream, return the string we have so far if (nextChar == -1) - return stringBuilder.ToString(); - - stringBuilder.Append((char)nextChar); + return new string(characters, 0, (int)i); + + characters[i] = (char)nextChar; } - - return stringBuilder.ToString(); + + return new string(characters); } /// @@ -650,13 +681,19 @@ private Result ReadStringWithNullTerminator(UIntPtr a // Get a stream that reads bytes from the process memory, and use a stream reader to read characters until we // find a null terminator. + // Performance note: Because we cannot know in advance how long the string is going to be, we have to use + // default values for buffer size and string builder size. + // This can result in wasteful memory usage, or multiple read operations, depending on the size of the string. + // But there is no way around this. + // Get a memory stream after the prefixes int stringBytesOffset = (settings.TypePrefix?.Length ?? 0) + (settings.LengthPrefix?.Size ?? 0); using var stream = GetMemoryStream(address + stringBytesOffset); - using var streamReader = new StreamReader(stream, settings.Encoding); + using var streamReader = new StreamReader(stream, settings.Encoding, false, StringReadingBufferDefaultSize, + false); // Read characters until we reach a null terminator (0) or the end of the stream (-1) - var stringBuilder = new StringBuilder(16); // Arbitrary initial size because we don't know the string length + var stringBuilder = new StringBuilder(StringReadingBuilderDefaultSize); for (var nextChar = streamReader.Read(); nextChar != -1 && nextChar != 0; nextChar = streamReader.Read()) { stringBuilder.Append((char)nextChar); diff --git a/test/MindControl.Benchmark/PointerPathBenchmark.cs b/test/MindControl.Benchmark/Benchmarks/PointerPathBenchmark.cs similarity index 91% rename from test/MindControl.Benchmark/PointerPathBenchmark.cs rename to test/MindControl.Benchmark/Benchmarks/PointerPathBenchmark.cs index 5b2b25a..cf0fbd3 100644 --- a/test/MindControl.Benchmark/PointerPathBenchmark.cs +++ b/test/MindControl.Benchmark/Benchmarks/PointerPathBenchmark.cs @@ -1,6 +1,6 @@ using BenchmarkDotNet.Attributes; -namespace MindControl.Benchmark; +namespace MindControl.Benchmark.Benchmarks; [MemoryDiagnoser] public class PointerPathBenchmark diff --git a/test/MindControl.Benchmark/ReadIntByAddressBenchmark.cs b/test/MindControl.Benchmark/Benchmarks/ReadIntByAddressBenchmark.cs similarity index 97% rename from test/MindControl.Benchmark/ReadIntByAddressBenchmark.cs rename to test/MindControl.Benchmark/Benchmarks/ReadIntByAddressBenchmark.cs index 43d33e1..97ab48a 100644 --- a/test/MindControl.Benchmark/ReadIntByAddressBenchmark.cs +++ b/test/MindControl.Benchmark/Benchmarks/ReadIntByAddressBenchmark.cs @@ -1,7 +1,7 @@ using System; using BenchmarkDotNet.Attributes; -namespace MindControl.Benchmark; +namespace MindControl.Benchmark.Benchmarks; [MemoryDiagnoser] [MarkdownExporter] diff --git a/test/MindControl.Benchmark/ReadLongByPointerPathBenchmark.cs b/test/MindControl.Benchmark/Benchmarks/ReadLongByPointerPathBenchmark.cs similarity index 98% rename from test/MindControl.Benchmark/ReadLongByPointerPathBenchmark.cs rename to test/MindControl.Benchmark/Benchmarks/ReadLongByPointerPathBenchmark.cs index 2463fe2..ffa165c 100644 --- a/test/MindControl.Benchmark/ReadLongByPointerPathBenchmark.cs +++ b/test/MindControl.Benchmark/Benchmarks/ReadLongByPointerPathBenchmark.cs @@ -1,7 +1,7 @@ using System; using BenchmarkDotNet.Attributes; -namespace MindControl.Benchmark; +namespace MindControl.Benchmark.Benchmarks; [MemoryDiagnoser] [MarkdownExporter] diff --git a/test/MindControl.Benchmark/Benchmarks/ReadRawStringBenchmark.cs b/test/MindControl.Benchmark/Benchmarks/ReadRawStringBenchmark.cs new file mode 100644 index 0000000..1683756 --- /dev/null +++ b/test/MindControl.Benchmark/Benchmarks/ReadRawStringBenchmark.cs @@ -0,0 +1,52 @@ +using System; +using System.Text; +using BenchmarkDotNet.Attributes; + +namespace MindControl.Benchmark.Benchmarks; + +[MemoryDiagnoser] +public class ReadRawStringBenchmark +{ + private BenchmarkMemorySetup _setup; + private string _pathString; + private PointerPath _pointerPath; + + [GlobalSetup] + public void GlobalSetup() + { + _setup = BenchmarkMemorySetup.Setup(); + _pathString = $"{_setup.OuterClassPointer:X}+8,C"; + _pointerPath = _pathString; + } + + [GlobalCleanup] + public void GlobalCleanup() => _setup.Dispose(); + + [Benchmark(Description = "MindControl ReadRawString (reused path)")] + public string MindControlReadRawStringReusedPath() + { + var result = _setup.MindControlProcessMemory.ReadRawString(_pointerPath, Encoding.Unicode).Value; + if (result != "ThisIsÄString") + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "MindControl ReadRawString (dynamic path)")] + public string MindControlReadRawStringDynamicPath() + { + var result = _setup.MindControlProcessMemory.ReadRawString(_pathString, Encoding.Unicode, 32, true).Value; + if (result != "ThisIsÄString") + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "Memory.dll ReadMemory")] + public string MemoryReadMemoryString() + { + var result = _setup.MemoryDllMem.ReadString(_pathString, length: 32, zeroTerminated: true, + stringEncoding: Encoding.Unicode); + if (result != "ThisIsÄString") + throw new Exception("Unexpected result"); + return result; + } +} \ No newline at end of file diff --git a/test/MindControl.Benchmark/Benchmarks/ReadStringPointerBenchmark.cs b/test/MindControl.Benchmark/Benchmarks/ReadStringPointerBenchmark.cs new file mode 100644 index 0000000..969d6f2 --- /dev/null +++ b/test/MindControl.Benchmark/Benchmarks/ReadStringPointerBenchmark.cs @@ -0,0 +1,56 @@ +using System; +using System.Text; +using BenchmarkDotNet.Attributes; + +namespace MindControl.Benchmark.Benchmarks; + +[MemoryDiagnoser] +public class ReadStringPointerBenchmark +{ + private BenchmarkMemorySetup _setup; + private string _pathString; + private PointerPath _pointerPath; + private UIntPtr _address; + private StringSettings _settings; + + [GlobalSetup] + public void GlobalSetup() + { + _setup = BenchmarkMemorySetup.Setup(); + _settings = new StringSettings(Encoding.Unicode, true, new StringLengthPrefix(4, StringLengthUnit.Characters), + new byte[8]); + _address = _setup.OuterClassPointer + 8; + _pathString = $"{_setup.OuterClassPointer:X}+8"; + _pointerPath = _pathString; + } + + [GlobalCleanup] + public void GlobalCleanup() => _setup.Dispose(); + + [Benchmark(Description = "MindControl ReadRawString (address)")] + public string MindControlReadRawStringAddress() + { + var result = _setup.MindControlProcessMemory.ReadStringPointer(_address, _settings).Value; + if (result != "ThisIsÄString") + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "MindControl ReadRawString (reused path)")] + public string MindControlReadRawStringReusedPath() + { + var result = _setup.MindControlProcessMemory.ReadStringPointer(_pointerPath, _settings).Value; + if (result != "ThisIsÄString") + throw new Exception("Unexpected result"); + return result; + } + + [Benchmark(Description = "MindControl ReadRawString (dynamic path)")] + public string MindControlReadRawStringDynamicPath() + { + var result = _setup.MindControlProcessMemory.ReadStringPointer(_pathString, _settings).Value; + if (result != "ThisIsÄString") + throw new Exception("Unexpected result"); + return result; + } +} \ No newline at end of file diff --git a/test/MindControl.Benchmark/WriteIntByAddressBenchmark.cs b/test/MindControl.Benchmark/Benchmarks/WriteIntByAddressBenchmark.cs similarity index 95% rename from test/MindControl.Benchmark/WriteIntByAddressBenchmark.cs rename to test/MindControl.Benchmark/Benchmarks/WriteIntByAddressBenchmark.cs index 15867d3..f911942 100644 --- a/test/MindControl.Benchmark/WriteIntByAddressBenchmark.cs +++ b/test/MindControl.Benchmark/Benchmarks/WriteIntByAddressBenchmark.cs @@ -1,7 +1,7 @@ using System; using BenchmarkDotNet.Attributes; -namespace MindControl.Benchmark; +namespace MindControl.Benchmark.Benchmarks; [MemoryDiagnoser] [MarkdownExporter] diff --git a/test/MindControl.Benchmark/WriteLongByPointerPathBenchmark.cs b/test/MindControl.Benchmark/Benchmarks/WriteLongByPointerPathBenchmark.cs similarity index 97% rename from test/MindControl.Benchmark/WriteLongByPointerPathBenchmark.cs rename to test/MindControl.Benchmark/Benchmarks/WriteLongByPointerPathBenchmark.cs index b797184..d337ab8 100644 --- a/test/MindControl.Benchmark/WriteLongByPointerPathBenchmark.cs +++ b/test/MindControl.Benchmark/Benchmarks/WriteLongByPointerPathBenchmark.cs @@ -1,7 +1,7 @@ using System; using BenchmarkDotNet.Attributes; -namespace MindControl.Benchmark; +namespace MindControl.Benchmark.Benchmarks; [MemoryDiagnoser] [MarkdownExporter] From ee0b5b744e9227bf57d626c842449e4d9e6975bb Mon Sep 17 00:00:00 2001 From: Doublevil Date: Fri, 14 Jun 2024 12:17:48 +0200 Subject: [PATCH 21/66] First hook method implementation --- MindControl.sln | 6 + src/MindControl.Code/Code/CodeChange.cs | 41 +++ .../Code/ProcessMemory.Code.cs | 81 +++++ .../Extensions/AssemblerExtensions.cs | 323 ++++++++++++++++++ src/MindControl.Code/Hooks/CodeHook.cs | 31 ++ src/MindControl.Code/Hooks/HookOptions.cs | 285 ++++++++++++++++ .../Hooks/ProcessMemory.Hooks.cs | 310 +++++++++++++++++ src/MindControl.Code/MindControl.Code.csproj | 35 ++ .../MindControl.Code.csproj.DotSettings | 2 + .../Results/CodeWritingFailure.cs | 116 +++++++ src/MindControl.Code/Results/HookFailure.cs | 151 ++++++++ src/MindControl/Addressing/MemoryRange.cs | 30 +- .../Addressing/PointerExtensions.cs | 43 +++ .../Addressing/ProcessMemoryStream.cs | 12 +- .../Allocation/MemoryReservation.cs | 8 +- src/MindControl/MindControl.csproj | 2 +- .../Native/IOperatingSystemService.cs | 26 +- .../Native/Win32Service.Imports.cs | 22 +- src/MindControl/Native/Win32Service.cs | 79 +---- .../ProcessMemory/ProcessMemory.Addressing.cs | 4 +- .../ProcessMemory/ProcessMemory.Allocation.cs | 116 +++++-- .../ProcessMemory.CodeInjection.cs | 8 +- .../ProcessMemory/ProcessMemory.FindBytes.cs | 4 +- .../ProcessMemory/ProcessMemory.Read.cs | 12 +- .../ProcessMemory/ProcessMemory.Write.cs | 10 +- .../ProcessMemory/ProcessMemory.cs | 34 +- src/MindControl/Results/Result.cs | 4 +- .../AddressingTests/MemoryRangeTest.cs | 31 +- .../AddressingTests/PointerExtensionsTest.cs | 65 ++++ .../AddressingTests/PointerPathTest.cs | 3 +- test/MindControl.Test/MindControl.Test.csproj | 20 +- .../CodeExtensions/ProcessMemoryHookTest.cs | 44 +++ .../ProcessMemoryAllocationTest.cs | 199 ++++++++++- .../ProcessMemoryEvaluateTest.cs | 4 +- .../ProcessMemoryFindBytesTest.cs | 8 +- .../ProcessMemoryReadTest.cs | 2 +- 36 files changed, 1953 insertions(+), 218 deletions(-) create mode 100644 src/MindControl.Code/Code/CodeChange.cs create mode 100644 src/MindControl.Code/Code/ProcessMemory.Code.cs create mode 100644 src/MindControl.Code/Extensions/AssemblerExtensions.cs create mode 100644 src/MindControl.Code/Hooks/CodeHook.cs create mode 100644 src/MindControl.Code/Hooks/HookOptions.cs create mode 100644 src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs create mode 100644 src/MindControl.Code/MindControl.Code.csproj create mode 100644 src/MindControl.Code/MindControl.Code.csproj.DotSettings create mode 100644 src/MindControl.Code/Results/CodeWritingFailure.cs create mode 100644 src/MindControl.Code/Results/HookFailure.cs create mode 100644 src/MindControl/Addressing/PointerExtensions.cs create mode 100644 test/MindControl.Test/AddressingTests/PointerExtensionsTest.cs create mode 100644 test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookTest.cs diff --git a/MindControl.sln b/MindControl.sln index 6aa1161..27c4af0 100644 --- a/MindControl.sln +++ b/MindControl.sln @@ -13,6 +13,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MindControl.Test.InjectedLi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindControl.Benchmark", "test\MindControl.Benchmark\MindControl.Benchmark.csproj", "{5C1B693D-D176-41B2-A47A-E78E098171CB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindControl.Code", "src\MindControl.Code\MindControl.Code.csproj", "{BF166555-9220-42C6-A93E-EC9FBA9AC38C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {5C1B693D-D176-41B2-A47A-E78E098171CB}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C1B693D-D176-41B2-A47A-E78E098171CB}.Release|Any CPU.ActiveCfg = Release|Any CPU {5C1B693D-D176-41B2-A47A-E78E098171CB}.Release|Any CPU.Build.0 = Release|Any CPU + {BF166555-9220-42C6-A93E-EC9FBA9AC38C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF166555-9220-42C6-A93E-EC9FBA9AC38C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF166555-9220-42C6-A93E-EC9FBA9AC38C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF166555-9220-42C6-A93E-EC9FBA9AC38C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/MindControl.Code/Code/CodeChange.cs b/src/MindControl.Code/Code/CodeChange.cs new file mode 100644 index 0000000..4e852a4 --- /dev/null +++ b/src/MindControl.Code/Code/CodeChange.cs @@ -0,0 +1,41 @@ +using MindControl.Results; + +namespace MindControl.Code; + +/// +/// Represents a modification to a code section in the memory of a process. +/// Allows reverting the alteration by writing the original bytes back to the code section. +/// +public class CodeChange +{ + private readonly ProcessMemory _processMemory; + private readonly byte[] _originalBytes; + + /// + /// Gets the address of the first byte of the modified code section. + /// + public UIntPtr Address { get; } + + /// + /// Gets the length in bytes of the modified code section. + /// + public int Length => _originalBytes.Length; + + /// + /// Initializes a new instance of the class. + /// + /// Process memory instance that contains the code section. + /// Address of the first byte of the code section. + /// Original bytes that were replaced by the alteration. + internal CodeChange(ProcessMemory processMemory, UIntPtr address, byte[] originalBytes) + { + _processMemory = processMemory; + _originalBytes = originalBytes; + Address = address; + } + + /// + /// Reverts the code alteration, writing the original bytes back to the code section. + /// + public Result Revert() => _processMemory.WriteBytes(Address, _originalBytes); +} diff --git a/src/MindControl.Code/Code/ProcessMemory.Code.cs b/src/MindControl.Code/Code/ProcessMemory.Code.cs new file mode 100644 index 0000000..3d7c6ba --- /dev/null +++ b/src/MindControl.Code/Code/ProcessMemory.Code.cs @@ -0,0 +1,81 @@ +using Iced.Intel; +using MindControl.Results; + +namespace MindControl.Code; + +/// +/// Provides extension methods for related to executable memory. +/// +public static class ProcessMemoryCodeExtensions +{ + /// NOP instruction opcode. + public const byte NopByte = 0x90; + + /// + /// Replaces the instruction (or multiple consecutive instructions) referenced by the given pointer path with NOP + /// instructions that do nothing, effectively disabling the original code. + /// + /// Process memory instance to use. + /// Path to the address of the first instruction to replace. + /// Number of consecutive instructions to replace. Default is 1. + /// A result holding either a code change instance, allowing you to revert modifications, or a code writing + /// failure. + public static Result RemoveCodeAt(this ProcessMemory processMemory, + PointerPath pointerPath, int instructionCount = 1) + { + if (instructionCount < 1) + return new CodeWritingFailureOnInvalidArguments( + "The number of instructions to replace must be at least 1."); + + var addressResult = processMemory.EvaluateMemoryAddress(pointerPath); + if (addressResult.IsFailure) + return new CodeWritingFailureOnPathEvaluation(addressResult.Error); + + return processMemory.RemoveCodeAt(addressResult.Value, instructionCount); + } + + /// + /// Replaces the instruction (or multiple consecutive instructions) at the given address with NOP instructions that + /// do nothing, effectively disabling the original code. + /// + /// Process memory instance to use. + /// Address of the first instruction to replace. + /// Number of consecutive instructions to replace. Default is 1. + /// A result holding either a code change instance, allowing you to revert modifications, or a code writing + /// failure. + public static Result RemoveCodeAt(this ProcessMemory processMemory, + UIntPtr address, int instructionCount = 1) + { + if (instructionCount < 1) + return new CodeWritingFailureOnInvalidArguments( + "The number of instructions to replace must be at least 1."); + + // For convenience, this method uses an instruction count, not a byte count. + // The problem is that instructions can take from 1 to 15 bytes, so we need to use a decoder to know how many + // bytes to replace. + + using var stream = processMemory.GetMemoryStream(address); + var codeReader = new StreamCodeReader(stream); + var decoder = Decoder.Create(processMemory.Is64Bits ? 64 : 32, codeReader); + + ulong fullLength = 0; + for (int i = 0; i < instructionCount; i++) + { + var instruction = decoder.Decode(); + if (instruction.IsInvalid) + return new CodeWritingFailureOnDecodingFailure(decoder.LastError); + + fullLength += (ulong)instruction.Length; + } + + var originalBytesResult = processMemory.ReadBytes(address, (int)fullLength); + if (originalBytesResult.IsFailure) + return new CodeWritingFailureOnReadFailure(originalBytesResult.Error); + + var nopInstructions = new byte[fullLength]; + nopInstructions.AsSpan().Fill(NopByte); + processMemory.WriteBytes(address, nopInstructions); + + return new CodeChange(processMemory, address, originalBytesResult.Value); + } +} \ No newline at end of file diff --git a/src/MindControl.Code/Extensions/AssemblerExtensions.cs b/src/MindControl.Code/Extensions/AssemblerExtensions.cs new file mode 100644 index 0000000..7b782f3 --- /dev/null +++ b/src/MindControl.Code/Extensions/AssemblerExtensions.cs @@ -0,0 +1,323 @@ +using Iced.Intel; +using MindControl.Results; + +namespace MindControl; + +/// +/// Provides extension methods around code assembling. +/// +public static class AssemblerExtensions +{ + /// + /// Checks if the register is compatible with the target architecture and can be saved and restored individually. + /// + /// Register to check. + /// True if the target architecture is 64 bits, false if it is 32 bits. + /// True if the register is compatible with the target architecture, false otherwise. + /// For ST registers, this method will always return false, because ST registers must be saved and restored + /// as a whole (to preserve the whole FPU stack). + internal static bool IsIndividualPreservationSupported(this Register register, bool is64Bits) + { + return (is64Bits && register.IsGPR64()) || (!is64Bits && register.IsGPR32()) + || (register.IsXMM() && register.GetNumber() <= GetMaxXmmRegisterNumber(is64Bits)) + || register.IsST() + || register.IsMM(); + } + + /// + /// Gets the maximum number of XMM registers available for the target architecture. + /// + /// True if the target architecture is 64 bits, false if it is 32 bits. + /// The maximum number of XMM registers available for the target architecture. + private static int GetMaxXmmRegisterNumber(bool is64Bits) => is64Bits ? 15 : 7; + + /// + /// Saves the given register to the stack (push it on the stack using instructions that depend on the register and + /// bitness). + /// + /// Assembler instance to use to generate the instructions. + /// Register to save. + /// Thrown if the register is not supported. + public static void SaveRegister(this Assembler assembler, Register register) + { + if (register.IsGPR32()) + assembler.push(new AssemblerRegister32(register)); + else if (register.IsGPR64()) + assembler.push(new AssemblerRegister64(register)); + else if (register.IsST()) + throw new ArgumentException( + $"Cannot save ST registers individually. They must be saved as a whole using the {nameof(SaveFpuStack)} method."); + else if (register.IsMM()) + { + if (assembler.Bitness == 64) + { + var rsp = new AssemblerRegister64(Register.RSP); + assembler.sub(rsp, 8); + assembler.movq(rsp+0, new AssemblerRegisterMM(register)); + } + else + { + var esp = new AssemblerRegister32(Register.ESP); + assembler.sub(esp, 8); + assembler.movq(esp+0, new AssemblerRegisterMM(register)); + } + } + else if (register.IsXMM()) + { + if (assembler.Bitness == 64) + { + var rsp = new AssemblerRegister64(Register.RSP); + assembler.sub(rsp, 16); + assembler.movq(rsp+0, new AssemblerRegisterXMM(register)); + } + else + { + var esp = new AssemblerRegister32(Register.ESP); + assembler.sub(esp, 16); + assembler.movq(esp+0, new AssemblerRegisterXMM(register)); + } + } + else + throw new ArgumentException($"Cannot save register {register} because it is not supported."); + } + + /// Registers that are pushed and popped on 2-byte instructions. + private static readonly Register[] TwoBytePushGprRegisters = + [ + Register.R8, Register.R9, Register.R10, Register.R11, Register.R12, Register.R13, Register.R14, Register.R15 + ]; + + /// + /// Gets the size in bytes of the instructions needed to save the given register to the stack. + /// + /// Register to save. + /// True if the target architecture is 64 bits, false if it is 32 bits. + /// The size in bytes of the instructions needed to save the register to the stack. + /// Thrown if the register is not supported. + internal static int GetSizeOfSaveInstructions(Register register, bool is64Bits) + { + // Some registers are pushed on 2-byte instructions + if (TwoBytePushGprRegisters.Contains(register)) + return 2; + + if (register.IsGPR32() || register.IsGPR64()) + return 1; // Other GPR registers are pushed on 1-byte + + if (register.IsMM() || register.IsXMM()) + { + // Example in x86: + // sub esp, 16 ; 83 EC 10 (3 bytes) + // movq [esp], xmm0 ; 0F 7F 04 24 (4 bytes) + + // For x64, there is an added REX prefix byte (0x48) for both instructions. + // Additionally, the movq instruction for XMM registers with numbers 8 and above (only available in x64) is + // one byte longer. + + int subSize = is64Bits ? 4 : 3; + int movSize = is64Bits ? (register.GetNumber() >= 8 ? 6 : 5) : 4; + return subSize + movSize; + } + + if (register.IsST()) + throw new ArgumentException( + $"Cannot evaluate ST registers individually. They must be evaluated as a whole using the {nameof(GetSizeOfFpuStackSave)} method."); + + throw new ArgumentException($"Cannot save register {register} because it is not supported."); + } + + /// + /// Restores the given register from the stack (pop it from the stack using instructions that depend on the register + /// and bitness). + /// + /// Assembler instance to use to generate the instructions. + /// Register to restore. + public static void RestoreRegister(this Assembler assembler, Register register) + { + if (register.IsGPR32()) + assembler.pop(new AssemblerRegister32(register)); + else if (register.IsGPR64()) + assembler.pop(new AssemblerRegister64(register)); + else if (register.IsST()) + throw new ArgumentException( + $"Cannot restore ST registers individually. They must be restored as a whole using the {nameof(RestoreFpuStack)} method."); + else if (register.IsMM()) + { + if (assembler.Bitness == 64) + { + var rsp = new AssemblerRegister64(Register.RSP); + assembler.movq(new AssemblerRegisterMM(register), rsp+0); + assembler.add(rsp, 8); + } + else + { + var esp = new AssemblerRegister32(Register.ESP); + assembler.movq(new AssemblerRegisterMM(register), esp+0); + assembler.add(esp, 8); + } + } + else if (register.IsXMM()) + { + if (assembler.Bitness == 64) + { + var rsp = new AssemblerRegister64(Register.RSP); + assembler.movq(new AssemblerRegisterXMM(register), rsp+0); + assembler.add(rsp, 16); + } + else + { + var esp = new AssemblerRegister32(Register.ESP); + assembler.movq(new AssemblerRegisterXMM(register), esp+0); + assembler.add(esp, 16); + } + } + else + throw new ArgumentException($"Cannot restore register {register} because it is not supported."); + } + + /// + /// Gets the size in bytes of the instructions needed to restore the given register to the stack. + /// + /// Register to save. + /// True if the target architecture is 64 bits, false if it is 32 bits. + /// The size in bytes of the instructions needed to restore the register from the stack. + /// Thrown if the register is not supported. + internal static int GetSizeOfRestoreInstructions(Register register, bool is64Bits) + => GetSizeOfSaveInstructions(register, is64Bits); // The instructions are the opposite, but the size is the same + + /// + /// Saves the FPU stack state as a whole (pushes all 8 ST registers to the stack). + /// This cannot be done individually because ST registers are part of a stack and thus save and restore operations + /// must be performed coordinatedly. + /// + /// Assembler instance to use to generate the instructions. + public static void SaveFpuStack(this Assembler assembler) + { + // The FPU stack is a stack of 8 ST registers, so we must save and restore them as a whole. + // Each ST register is 10 bytes long, so we should push 80 bytes to the stack. + // However, for alignment purposes, we consider them as 12 bytes each, and so we push 96 bytes to the stack. + // This allows for optimal memory access. + + if (assembler.Bitness == 64) + { + var rsp = new AssemblerRegister64(Register.RSP); + assembler.sub(rsp, 12 * 8); // Allocate space for all 8 FPU values + for (int i = 0; i < 8; i++) + assembler.fstp(rsp + i * 12); + } + else + { + var esp = new AssemblerRegister32(Register.ESP); + assembler.sub(esp, 12 * 8); // Allocate space for all 8 FPU values + for (int i = 0; i < 8; i++) + assembler.fstp(esp + i * 12); + } + } + + /// + /// Gets the size in bytes of the instructions needed to save the FPU stack state as a whole. + /// + /// True if the target architecture is 64 bits, false if it is 32 bits. + /// The size in bytes of the instructions needed to save the FPU stack state as a whole. + internal static int GetSizeOfFpuStackSave(bool is64Bits) + { + // Example in x86: + // sub esp, 64 ; 83 EC 40 (3 bytes) + // fstp [esp] ; D9 1C 24 (3 bytes) + // fstp [esp+8] ; D9 5C 24 08 (4 bytes) + // ... + // fstp [esp+54] ; D9 5C 24 38 (4 bytes) + + // For x64, there is an added REX prefix byte (0x48) for each instruction. + // So it should be 3 + 3 + 4 * 7 = 6 + 28 = 34 bytes for x86 and 4 + 4 + 5 * 7 = 8 + 35 = 43 bytes for x64. + + return is64Bits ? 43 : 34; + } + + /// + /// Restores the FPU stack state as a whole (pops all 8 ST registers from the stack). + /// This cannot be done individually because ST registers are part of a stack and thus save and restore operations + /// must be performed coordinatedly. + /// + /// Assembler instance to use to generate the instructions. + public static void RestoreFpuStack(this Assembler assembler) + { + if (assembler.Bitness == 64) + { + var rsp = new AssemblerRegister64(Register.RSP); + for (int i = 7; i >= 0; i--) + assembler.fld(rsp + i * 12); + assembler.add(rsp, 12 * 8); // Deallocate space for all 8 FPU values + } + else + { + var esp = new AssemblerRegister32(Register.ESP); + for (int i = 7; i >= 0; i--) + assembler.fld(esp + i * 12); + assembler.add(esp, 12 * 8); // Deallocate space for all 8 FPU values + } + } + + /// + /// Gets the size in bytes of the instructions needed to restore the FPU stack state as a whole. + /// + /// True if the target architecture is 64 bits, false if it is 32 bits. + /// The size in bytes of the instructions needed to restore the FPU stack state as a whole. + internal static int GetSizeOfFpuStackRestore(bool is64Bits) + => GetSizeOfFpuStackSave(is64Bits); // The instructions are the opposite, but the size is the same + + /// + /// Saves the CPU flags to the stack (pushes the flags to the stack). + /// + /// Assembler instance to use to generate the instructions. + internal static void SaveFlags(this Assembler assembler) + { + if (assembler.Bitness == 64) + assembler.pushfq(); + else + assembler.pushf(); + } + + /// + /// Gets the size in bytes of the instructions needed to save the CPU flags. + /// + /// True if the target architecture is 64 bits, false if it is 32 bits. + /// The size in bytes of the instructions needed to save the CPU flags. + internal static int GetSizeOfFlagsSave(bool is64Bits) => 1; // pushf is always single-byte + + /// + /// Restores the CPU flags from the stack (pops the flags from the stack). + /// + /// Assembler instance to use to generate the instructions. + internal static void RestoreFlags(this Assembler assembler) + { + if (assembler.Bitness == 64) + assembler.popfq(); + else + assembler.popf(); + } + + /// + /// Gets the size in bytes of the instructions needed to restore the CPU flags. + /// + /// True if the target architecture is 64 bits, false if it is 32 bits. + /// The size in bytes of the instructions needed to restore the CPU flags. + internal static int GetSizeOfFlagsRestore(bool is64Bits) => 1; // popf is always single-byte + + /// + /// Attempts to assemble the instructions registered on this assembler instance and return the resulting bytes. + /// + /// Assembler instance to use to generate the instructions. + /// Base address to use for the assembled instructions. Default is 0. + /// Size of the buffer to use for the assembled instructions. Default is 128 bytes. + /// A result holding either the assembled instructions as a byte array, or an error message if the + /// assembling failed. + public static Result AssembleToBytes(this Assembler assembler, ulong baseAddress = 0, + int bufferSize = 128) + { + using var memoryStream = new MemoryStream(bufferSize); + var writer = new StreamCodeWriter(memoryStream); + if (!assembler.TryAssemble(writer, baseAddress, out var error, out _)) + return error; + return memoryStream.ToArray(); + } +} \ No newline at end of file diff --git a/src/MindControl.Code/Hooks/CodeHook.cs b/src/MindControl.Code/Hooks/CodeHook.cs new file mode 100644 index 0000000..0904490 --- /dev/null +++ b/src/MindControl.Code/Hooks/CodeHook.cs @@ -0,0 +1,31 @@ +using MindControl.Code; + +namespace MindControl.Hooks; + +/// +/// Represents a modification to a code section in the memory of a process. +/// The modification is a hook, which replaces code instructions with a jump to a block of injected code. +/// Allows reverting the jump by writing the original instructions back to the code section. Reverting does not remove +/// the injected code, but will remove the entry point to the code. +/// +public class CodeHook : CodeChange +{ + /// + /// Gets the reservation holding the injected code. + /// + public MemoryReservation InjectedCodeReservation { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Process memory instance that contains the code section. + /// Address of the first byte of the code section. + /// Original bytes that were replaced by the alteration. + /// Reservation holding the injected code. + internal CodeHook(ProcessMemory processMemory, UIntPtr address, byte[] originalBytes, + MemoryReservation injectedCodeReservation) + : base(processMemory, address, originalBytes) + { + InjectedCodeReservation = injectedCodeReservation; + } +} \ No newline at end of file diff --git a/src/MindControl.Code/Hooks/HookOptions.cs b/src/MindControl.Code/Hooks/HookOptions.cs new file mode 100644 index 0000000..8b614b8 --- /dev/null +++ b/src/MindControl.Code/Hooks/HookOptions.cs @@ -0,0 +1,285 @@ +using Iced.Intel; + +namespace MindControl.Hooks; + +/// +/// Defines how the injected code is executed in relation to the original code. +/// +public enum HookExecutionMode +{ + /// + /// Executes the injected code before the original target instruction. + /// + ExecuteInjectedCodeFirst, + + /// + /// Executes the original target instruction first, and then the injected code. If additional instructions are + /// overwritten by the hook jump, they will be executed after the injected code. + /// + ExecuteOriginalInstructionFirst, + + /// + /// Executes only the injected code, overwriting the instruction at the hook address. If additional instructions are + /// overwritten by the hook jump, they will be executed after the injected code. + /// + ReplaceOriginalInstruction +} + +/// +/// Flags that define how the injected code is isolated from the original code. +/// +[Flags] +public enum HookIsolationMode +{ + /// + /// No isolation. Modifications to flags and registers in the injected code will affect the original code, unless + /// the injected code contains instructions to save and restore the state in itself. + /// + None = 0, + + /// + /// Save and restore CPU flags. This allows the injected code to modify flags without affecting the original code. + /// This option is a flag, it can be combined with other options. + /// + PreserveFlags = 1, + + /// + /// Save and restore general-purpose registers. This allows the injected code to modify most registers without + /// affecting the original code. + /// This option is a flag, it can be combined with other options. + /// + PreserveGeneralPurposeRegisters = 2, + + /// + /// Save and restore XMM registers. This allows the injected code to modify XMM registers (for floating-point + /// operations) without affecting the original code. + /// This option is a flag, it can be combined with other options. + /// + PreserveXmmRegisters = 4, + + /// + /// Save and restore the FPU stack. This allows the injected code to modify the FPU stack without affecting the + /// original code. Note that the FPU stack is not used in modern code, so this option is usually not needed. + /// This option is a flag, it can be combined with other options. + /// + PreserveFpuStack = 8, + + /// + /// Save and restore the FPU stack. This allows the injected code to modify the FPU stack without affecting the + /// original code. Note that the FPU stack is not used in modern code, so this option is usually not needed. + /// This option is a flag, it can be combined with other options. + /// + PreserveMmRegisters = 16, + + /// + /// This option is a combination of the , , + /// and options. + /// This allows the injected code to modify flags and registers without affecting the original code. It is the + /// recommended option. + /// + FullIsolation = PreserveFlags | PreserveGeneralPurposeRegisters | PreserveXmmRegisters, + + /// + /// This option is a combination of all isolation options. + /// It allows the injected code to modify flags and registers without affecting the original code. It also includes + /// the FPU stack and MM registers, which are usually not needed. This will make the hook code slower. + /// Prefer unless you know ST and MM registers are used both in the original code and + /// the injected code. + /// + FullCompatibilityIsolation = FullIsolation | PreserveFpuStack | PreserveMmRegisters +} + +/// +/// Defines how the hook jump should be performed. +/// +public enum HookJumpMode +{ + /// + /// Use a near jump if possible. If a near jump is not possible, fall back to a far jump. + /// This is the safest option, as it will always work, but may not always give you the best performance (although + /// it should in most cases). + /// + NearJumpWithFallbackOnFarJump, + + /// + /// Use a near jump only. If a near jump is not possible, the hook operation will fail. + /// Use this only if hook performance is critical and a far jump would not be acceptable. + /// + NearJumpOnly +} + +/// +/// Holds settings for a hook operation. +/// +public class HookOptions +{ + /// + /// Lists the general purpose registers that are preserved by the + /// isolation flag. + /// + private static readonly Register[] PreservableGeneralPurposeRegisters = + [ + // General purpose 32-bit registers + Register.EAX, Register.EBX, Register.ECX, Register.EDX, + Register.ESI, Register.EDI, Register.EBP, Register.ESP, + + // General purpose 64-bit registers + Register.RAX, Register.RBX, Register.RCX, Register.RDX, + Register.RSI, Register.RDI, Register.RBP, Register.RSP, + Register.R8, Register.R9, Register.R10, Register.R11, + Register.R12, Register.R13, Register.R14, Register.R15 + ]; + + /// + /// Lists the XMM registers to be preserved by the isolation + /// flag. + /// + private static readonly Register[] XmmRegisters = + [ + // (8-15 are 64-bit only) + Register.XMM0, Register.XMM1, Register.XMM2, Register.XMM3, + Register.XMM4, Register.XMM5, Register.XMM6, Register.XMM7, + Register.XMM8, Register.XMM9, Register.XMM10, Register.XMM11, + Register.XMM12, Register.XMM13, Register.XMM14, Register.XMM15 + ]; + + /// + /// Lists the MM registers to be preserved by the isolation + /// flag. + /// + private static readonly Register[] MmRegisters = + [ + Register.MM0, Register.MM1, Register.MM2, Register.MM3, + Register.MM4, Register.MM5, Register.MM6, Register.MM7 + ]; + + /// + /// Gets the execution mode of the injected code in relation to the original code. Defines whether the original + /// code should be overwritten, executed before, or executed after the injected code. + /// + public HookExecutionMode ExecutionMode { get; init; } + + /// + /// Gets the isolation mode flags defining how the injected code is isolated from the original code. + /// Depending on the mode, instructions may be prepended and appended to the injected code to save and restore + /// registers and flags, allowing the injected code to run without affecting the original code. + /// + public HookIsolationMode IsolationMode { get; init; } + + /// + /// Gets the registers to exclude from preservation, regardless of the . + /// Use this if you want specific registers to NOT be saved and restored, either to improve performance or to + /// modify the behavior of the original code. + /// + public Register[] RegistersExcludedFromPreservation { get; init; } + + /// + /// Gets the jump mode, which defines what kind of jump should be used to redirect the code flow to the injected + /// code. Most of the time, you should leave it to its default + /// value. + /// + public HookJumpMode JumpMode { get; init; } + + /// + /// Initializes a new instance of the class with the given values. + /// The is set to . + /// + /// Execution mode of the injected code in relation to the original code. Defines + /// whether the original code should be overwritten, executed before, or executed after the injected code. + /// Optional registers to exclude from preservation, regardless of + /// the . Use this if you want specific registers to NOT be saved and restored, either to + /// improve performance or to modify the behavior of the original code. + public HookOptions(HookExecutionMode executionMode, params Register[] registersExcludedFromPreservation) + : this(executionMode, HookIsolationMode.FullIsolation, HookJumpMode.NearJumpWithFallbackOnFarJump, + registersExcludedFromPreservation) { } + + /// + /// Initializes a new instance of the class with the given values. + /// + /// Execution mode of the injected code in relation to the original code. Defines + /// whether the original code should be overwritten, executed before, or executed after the injected code. + /// Isolation mode flags defining how the injected code is isolated from the original + /// code. Depending on the mode, instructions may be prepended and appended to the injected code to save and + /// restore registers and flags, allowing the injected code to run without affecting the original code. + /// Optional registers to exclude from preservation, regardless of + /// the . Use this if you want specific registers to NOT be saved and restored, + /// either to improve performance or to modify the behavior of the original code. + public HookOptions(HookExecutionMode executionMode, HookIsolationMode isolationMode, + params Register[] registersExcludedFromPreservation) + : this(executionMode, isolationMode, HookJumpMode.NearJumpWithFallbackOnFarJump, + registersExcludedFromPreservation) { } + + /// + /// Initializes a new instance of the class with the given values. + /// + /// Execution mode of the injected code in relation to the original code. Defines + /// whether the original code should be overwritten, executed before, or executed after the injected code. + /// Isolation mode flags defining how the injected code is isolated from the original + /// code. Depending on the mode, instructions may be prepended and appended to the injected code to save and + /// restore registers and flags, allowing the injected code to run without affecting the original code. + /// Jump mode, which defines what kind of jump should be used to redirect the code flow to + /// the injected code. Use , unless performance is critical + /// and a far jump would be unacceptable. + /// Optional registers to exclude from preservation, regardless of + /// the . Use this if you want specific registers to NOT be saved and restored, + /// either to improve performance or to modify the behavior of the original code. + public HookOptions(HookExecutionMode executionMode, HookIsolationMode isolationMode, HookJumpMode jumpMode, + params Register[] registersExcludedFromPreservation) + { + ExecutionMode = executionMode; + IsolationMode = isolationMode; + JumpMode = jumpMode; + RegistersExcludedFromPreservation = registersExcludedFromPreservation; + _registersToPreserve = GetRegistersToPreserve(); + } + + private readonly Register[] _registersToPreserve; + + /// + /// Gets the registers to save and restore, based on the and + /// properties. + /// + internal IEnumerable RegistersToPreserve => _registersToPreserve; + + /// + /// Builds the array of registers to save and restore, based on the and + /// properties. + /// + private Register[] GetRegistersToPreserve() + { + List registersToPreserve = new(64); + if (IsolationMode.HasFlag(HookIsolationMode.PreserveGeneralPurposeRegisters)) + registersToPreserve.AddRange(PreservableGeneralPurposeRegisters); + if (IsolationMode.HasFlag(HookIsolationMode.PreserveXmmRegisters)) + registersToPreserve.AddRange(XmmRegisters); + if (IsolationMode.HasFlag(HookIsolationMode.PreserveMmRegisters)) + registersToPreserve.AddRange(MmRegisters); + + return registersToPreserve.Except(RegistersExcludedFromPreservation).ToArray(); + } + + /// + /// Gets the predicted length in bytes of the additional code that will be prepended and appended to the hook code. + /// + /// Indicates if the target process is 64-bit. + /// The total size in bytes of the additional code. + internal int GetExpectedGeneratedCodeSize(bool is64Bits) + { + int totalSize = 0; + if (IsolationMode.HasFlag(HookIsolationMode.PreserveFlags)) + totalSize += AssemblerExtensions.GetSizeOfFlagsSave(is64Bits) + + AssemblerExtensions.GetSizeOfFlagsRestore(is64Bits); + + foreach (var register in RegistersToPreserve) + { + totalSize += AssemblerExtensions.GetSizeOfSaveInstructions(register, is64Bits); + totalSize += AssemblerExtensions.GetSizeOfRestoreInstructions(register, is64Bits); + } + + if (IsolationMode.HasFlag(HookIsolationMode.PreserveFpuStack)) + totalSize += AssemblerExtensions.GetSizeOfFpuStackSave(is64Bits) + + AssemblerExtensions.GetSizeOfFpuStackRestore(is64Bits); + + return totalSize; + } +} \ No newline at end of file diff --git a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs new file mode 100644 index 0000000..5f9405a --- /dev/null +++ b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs @@ -0,0 +1,310 @@ +using Iced.Intel; +using MindControl.Code; +using MindControl.Results; + +namespace MindControl.Hooks; + +/// +/// Provides extension methods for related to code hooks. +/// +public static class ProcessMemoryHookExtensions +{ + /// + /// Byte count for a near jump instruction. + /// + private const int NearJumpInstructionLength = 5; + + /// + /// Byte count for a far jump instruction. + /// 64-bit far jumps can take up to 15 bytes to encode, because the full address to jump to must be written in the + /// code, and aligned properly. + /// + private const int FarJumpInstructionLength = 15; + + /// Maximum byte count for a single instruction. + private const int MaxInstructionLength = 15; + + /// + /// Injects code into the target process to be executed when the instruction at the given executable address is + /// reached. + /// Execution of the original code will continue normally (unless the given hook code is designed otherwise). + /// With default options, some code will be generated to save and restore state, so that the hook code is isolated + /// from the original code. + /// + /// Process memory instance to use. + /// Address of the first byte of the instruction to hook into. + /// Assembled code to inject into the target process. Warning: if your code contains + /// instructions with relative operands (like most jumps or calls), they will break. Use the + /// signature to make sure they still point to the + /// right address. + /// Options defining how the hook works. + /// A result holding either a code hook instance that contains a reference to the injected code reservation + /// and allows you to revert the hook, or a hook failure when the operation failed. + public static Result Hook(this ProcessMemory processMemory, UIntPtr targetInstructionAddress, + byte[] injectedCode, HookOptions options) + { + ulong sizeToReserve = (ulong)(options.GetExpectedGeneratedCodeSize(processMemory.Is64Bits) + + injectedCode.Length + + MaxInstructionLength // Extra room for the original instructions + + FarJumpInstructionLength); // Extra room for the jump back to the original code + + // Reserve memory for the injected code. Try to reserve memory close enough to the target instruction to use a + // near jump, if possible. + var reservationResult = ReserveNearHookTarget(processMemory, sizeToReserve, targetInstructionAddress); + if (reservationResult.IsFailure && options.JumpMode == HookJumpMode.NearJumpOnly) + return new HookFailureOnAllocationFailure(reservationResult.Error); + + if (reservationResult.IsFailure) + { + // If we cannot reserve memory for a near jump, try going for a far jump + reservationResult = processMemory.Reserve(sizeToReserve, true); + if (reservationResult.IsFailure) + return new HookFailureOnAllocationFailure(reservationResult.Error); + } + + // Assemble the jump to the injected code + var reservation = reservationResult.Value; + var jmpAssembler = new Assembler(processMemory.Is64Bits ? 64 : 32); + jmpAssembler.jmp(reservation.Address); + var jmpAssembleResult = jmpAssembler.AssembleToBytes(targetInstructionAddress); + if (jmpAssembleResult.IsFailure) + { + reservation.Dispose(); + return new HookFailureOnCodeAssembly(HookCodeAssemblySource.JumpToInjectedCode, jmpAssembleResult.Error); + } + + byte[] jmpBytes = jmpAssembleResult.Value; + + // Read the original instructions to replace, until we have enough bytes to fit the jump to the injected code + using var stream = processMemory.GetMemoryStream(targetInstructionAddress); + var codeReader = new StreamCodeReader(stream); + var decoder = Decoder.Create(processMemory.Is64Bits ? 64 : 32, codeReader, targetInstructionAddress); + var instructionsToReplace = new List(); + int bytesRead = 0; + while (bytesRead < jmpBytes.Length) + { + var instruction = decoder.Decode(); + if (instruction.IsInvalid) + { + reservation.Dispose(); + return new HookFailureOnDecodingFailure(decoder.LastError); + } + + instructionsToReplace.Add(instruction); + bytesRead += instruction.Length; + } + + // Read the original bytes to replace (so that the hook can be reverted) + var originalBytesResult = processMemory.ReadBytes(targetInstructionAddress, bytesRead); + if (originalBytesResult.IsFailure) + { + reservation.Dispose(); + return new HookFailureOnReadFailure(originalBytesResult.Error); + } + + // Pad the jump bytes with NOPs if needed (example: we inject a 5-byte jump, but we read 2 instructions of 3 + // bytes each at the target address, meaning we need to add one NOP instruction so that the jump instruction is + // now as long as the instructions it replaces). + if (bytesRead > jmpBytes.Length) + jmpBytes = jmpBytes.Concat(Enumerable.Repeat(ProcessMemoryCodeExtensions.NopByte, + bytesRead - jmpBytes.Length)).ToArray(); + + var nextOriginalInstructionAddress = (UIntPtr)(targetInstructionAddress + (ulong)bytesRead); + + // Assemble the pre-code + var preHookCodeResult = BuildPreHookCode(reservation.Address, instructionsToReplace, processMemory.Is64Bits, + options); + if (preHookCodeResult.IsFailure) + { + reservation.Dispose(); + return preHookCodeResult.Error; + } + byte[] preHookCodeBytes = preHookCodeResult.Value; + + // Assemble the post-code + var postHookCodeAddress = (UIntPtr)(reservation.Address + (ulong)preHookCodeBytes.Length + + (ulong)injectedCode.Length); + var postHookCodeResult = BuildPostHookCode(postHookCodeAddress, instructionsToReplace, processMemory.Is64Bits, + options, nextOriginalInstructionAddress); + if (postHookCodeResult.IsFailure) + { + reservation.Dispose(); + return postHookCodeResult.Error; + } + byte[] postHookCodeBytes = postHookCodeResult.Value; + + // Assemble the full code to inject + var fullCodeLength = preHookCodeBytes.Length + injectedCode.Length + postHookCodeBytes.Length; + if ((ulong)fullCodeLength > reservation.Size) + { + reservation.Dispose(); + return new HookFailureOnCodeAssembly(HookCodeAssemblySource.Unkown, + $"The assembled code is too large to fit in the reserved memory (reserved {reservation.Size} bytes, but assembled a total of {fullCodeLength} bytes). Please report this, as it is a bug in the hook code generation."); + } + var fullInjectedCode = new byte[fullCodeLength]; + Buffer.BlockCopy(preHookCodeBytes, 0, fullInjectedCode, 0, preHookCodeBytes.Length); + Buffer.BlockCopy(injectedCode, 0, fullInjectedCode, preHookCodeBytes.Length, injectedCode.Length); + Buffer.BlockCopy(postHookCodeBytes, 0, fullInjectedCode, preHookCodeBytes.Length + injectedCode.Length, + postHookCodeBytes.Length); + + // Write the assembled code to the reserved memory + var writeResult = processMemory.WriteBytes(reservation.Address, fullInjectedCode); + if (writeResult.IsFailure) + { + reservation.Dispose(); + return new HookFailureOnWriteFailure(writeResult.Error); + } + + // Write the jump to the injected code + var jmpWriteResult = processMemory.WriteBytes(targetInstructionAddress, jmpBytes); + if (jmpWriteResult.IsFailure) + { + reservation.Dispose(); + return new HookFailureOnWriteFailure(jmpWriteResult.Error); + } + + return new CodeHook(processMemory, targetInstructionAddress, originalBytesResult.Value, reservation); + } + + /// + /// Assembles the code that comes before the user-made injected code in a hook operation. + /// + /// Address where the pre-hook code is going to be written. + /// Original instructions to be replaced by a jump to the injected code. + /// Boolean indicating if the target process is 64-bits. + /// Options defining how the hook behaves. + /// A result holding either the assembled code bytes, or a hook failure. + private static Result BuildPreHookCode(ulong baseAddress, + IList instructionsToReplace, bool is64Bits, HookOptions options) + { + var assembler = new Assembler(is64Bits ? 64 : 32); + + // If the hook mode specifies that the original instruction should be executed first, add it to the code + if (options.ExecutionMode == HookExecutionMode.ExecuteOriginalInstructionFirst + && instructionsToReplace.Count > 0) + { + assembler.AddInstruction(instructionsToReplace.First()); + } + + // Save flags if needed + if (options.IsolationMode.HasFlag(HookIsolationMode.PreserveFlags)) + assembler.SaveFlags(); + + // Save registers if needed + foreach (var register in options.RegistersToPreserve) + { + // Skip the register if it's not supported or not compatible with the target architecture + if (!register.IsIndividualPreservationSupported(is64Bits)) + continue; + + assembler.SaveRegister(register); + } + + // Save the FPU stack if needed + if (options.IsolationMode.HasFlag(HookIsolationMode.PreserveFpuStack)) + assembler.SaveFpuStack(); + + // Assemble the code and return the resulting bytes + var result = assembler.AssembleToBytes(baseAddress, 128); + if (result.IsFailure) + return new HookFailureOnCodeAssembly(HookCodeAssemblySource.PrependedCode, result.Error); + + return result.Value; + } + + /// + /// Assembles the code that comes after the user-made injected code in a hook operation. + /// + /// Address where the post-hook code is going to be written. + /// Original instructions to be replaced by a jump to the injected code. + /// Boolean indicating if the target process is 64-bits. + /// Options defining how the hook behaves. + /// Address of the first byte of the instruction to jump back to. + /// A result holding either the assembled code bytes, or a hook failure. + private static Result BuildPostHookCode(ulong baseAddress, + IList instructionsToReplace, bool is64Bits, HookOptions options, UIntPtr originalCodeJumpTarget) + { + var assembler = new Assembler(is64Bits ? 64 : 32); + + // Restore the FPU stack if needed + if (options.IsolationMode.HasFlag(HookIsolationMode.PreserveFpuStack)) + assembler.RestoreFpuStack(); + + // Restore registers if needed + foreach (var register in options.RegistersToPreserve.Reverse()) + { + // Skip the register if it's not supported or not compatible with the target architecture + if (!register.IsIndividualPreservationSupported(is64Bits)) + continue; + + assembler.RestoreRegister(register); + } + + // Restore flags if needed + if (options.IsolationMode.HasFlag(HookIsolationMode.PreserveFlags)) + assembler.RestoreFlags(); + + // If the hook mode specifies that the hook code should be executed first, append the original instructions + if (options.ExecutionMode == HookExecutionMode.ExecuteInjectedCodeFirst + && instructionsToReplace.Count > 0) + { + assembler.AddInstruction(instructionsToReplace.First()); + } + + // In all cases, add any additional instructions that were replaced by the jump to the injected code + foreach (var instruction in instructionsToReplace.Skip(1)) + assembler.AddInstruction(instruction); + + // Jump back to the original code + assembler.jmp(originalCodeJumpTarget); + + // Assemble the code and return the resulting bytes + var result = assembler.AssembleToBytes(baseAddress, 128); + if (result.IsFailure) + return new HookFailureOnCodeAssembly(HookCodeAssemblySource.AppendedCode, result.Error); + + return result.Value; + } + + /// + /// Attempts to reserve executable memory close to the hook target instruction. + /// + /// Target process memory instance. + /// Size of the memory to reserve. + /// Address of the jump instruction that will jump to the injected code. + /// A result holding either the memory reservation, or an allocation failure. + private static Result ReserveNearHookTarget(ProcessMemory processMemory, + ulong sizeToReserve, UIntPtr jumpAddress) + { + // The range of a near jump is limited by the signed byte displacement that follows the opcode. + // The displacement is a signed 4-byte integer, so the range is from -2GB to +2GB. + + // In 32-bits processes, near jumps can be made to any address, despite the offset being a signed integer. + // For example, a jump from 0x00000000 to 0xFFFFFFFF can be made with a near jump with an offset of -1. + // Which means that for 32-bit processes, we can reserve memory anywhere in the address space. + if (!processMemory.Is64Bits) + return processMemory.Reserve(sizeToReserve, true, nearAddress: jumpAddress); + + // In 64-bits processes, however, the offset is still a signed 4-byte integer, but the full range is much + // larger, meaning we can't reach any address. + + // For practical purposes, we disregard wrap-around jumps for 64-bits processes (cases where the address plus + // the offset is outside the 64-bits address space), because they mostly make sense for 32-bits processes and + // would complicate things unnecessarily. + + // The displacement is relative to the address of the next instruction. A near jump instruction is 5 bytes long. + var nextInstructionAddress = jumpAddress + 5; + + // The aim of this method is to reserve memory close enough to perform a near jump from the target instruction. + // However, to potentially avoid having to do far jumps from the injected code, we do a first attempt to reserve + // memory that is extra close to the target instruction, if possible. + var extraCloseRange = nextInstructionAddress.GetRangeAround(int.MaxValue); + var extraCloseReservation = processMemory.Reserve(sizeToReserve, true, extraCloseRange, nextInstructionAddress); + if (extraCloseReservation.IsSuccess) + return extraCloseReservation; + + // If the extra close reservation failed, use the full range of a near jump + var nearJumpRange = nextInstructionAddress.GetRangeAround(uint.MaxValue); + return processMemory.Reserve(sizeToReserve, true, nearJumpRange, nextInstructionAddress); + } +} \ No newline at end of file diff --git a/src/MindControl.Code/MindControl.Code.csproj b/src/MindControl.Code/MindControl.Code.csproj new file mode 100644 index 0000000..6e24b45 --- /dev/null +++ b/src/MindControl.Code/MindControl.Code.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + enable + enable + MindControl + MindControl.Code + https://github.com/Doublevil/mind-control + git + 1.0.0 + + + + + true + True + Doublevil + An extension library for MindControl that gives access to advanced ASM code-related features like + hooks. + library, process, memory, win32, game, hacking, hack, trainer, cheat, cheating + MIT + icon.png + README.md + + + + + + + + + + + diff --git a/src/MindControl.Code/MindControl.Code.csproj.DotSettings b/src/MindControl.Code/MindControl.Code.csproj.DotSettings new file mode 100644 index 0000000..17962b1 --- /dev/null +++ b/src/MindControl.Code/MindControl.Code.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/MindControl.Code/Results/CodeWritingFailure.cs b/src/MindControl.Code/Results/CodeWritingFailure.cs new file mode 100644 index 0000000..d9b55f4 --- /dev/null +++ b/src/MindControl.Code/Results/CodeWritingFailure.cs @@ -0,0 +1,116 @@ +using Iced.Intel; + +namespace MindControl.Results; + +/// +/// Enumerates the possible reasons for a code writing failure. +/// +public enum CodeWritingFailureReason +{ + /// + /// The given pointer path could not be successfully evaluated. + /// + PathEvaluationFailure, + + /// + /// The arguments provided to the code write operation are invalid. + /// + InvalidArguments, + + /// + /// The target address is a zero pointer. + /// + ZeroPointer, + + /// + /// A reading operation failed. + /// + ReadFailure, + + /// + /// A code disassembling operation failed. + /// + DecodingFailure, + + /// + /// A write operation failed. + /// + WriteFailure +} + +/// +/// Represents a failure that occurred while writing code to a target process. +/// +/// Reason for the failure. +public abstract record CodeWritingFailure(CodeWritingFailureReason Reason); + +/// +/// Represents a failure that occurred while writing code to a target process when the pointer path failed to evaluate. +/// +/// Details about the path evaluation failure. +public record CodeWritingFailureOnPathEvaluation(PathEvaluationFailure Details) + : CodeWritingFailure(CodeWritingFailureReason.PathEvaluationFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failed to evaluate the given pointer path: {Details}"; +} + +/// +/// Represents a failure that occurred while writing code to a target process when the arguments provided are invalid. +/// +/// Message that describes how the arguments fail to meet expectations. +public record CodeWritingFailureOnInvalidArguments(string Message) + : CodeWritingFailure(CodeWritingFailureReason.InvalidArguments) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The provided arguments are invalid: {Message}"; +} + +/// +/// Represents a failure that occurred while writing code to a target process when the target address is a zero pointer. +/// +public record CodeWritingFailureOnZeroPointer() + : CodeWritingFailure(CodeWritingFailureReason.ZeroPointer) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => "The target address is a zero pointer."; +} + +/// +/// Represents a failure that occurred while writing code to a target process when a read operation failed. +/// +/// Details about the read failure. +public record CodeWritingFailureOnReadFailure(ReadFailure Details) + : CodeWritingFailure(CodeWritingFailureReason.ReadFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failed to read code from the target address: {Details}"; +} + +/// +/// Represents a failure that occurred while writing code to a target process when a disassembling operation failed. +/// +/// Error code that describes the failure. +public record CodeWritingFailureOnDecodingFailure(DecoderError Error) + : CodeWritingFailure(CodeWritingFailureReason.DecodingFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failed to decode the instruction with the following error code: {Error}"; +} + +/// +/// Represents a failure that occurred while writing code to a target process when a write operation failed. +/// +/// Details about the write failure. +public record CodeWritingFailureOnWriteFailure(WriteFailure Details) + : CodeWritingFailure(CodeWritingFailureReason.WriteFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failed to write code to the target address: {Details}"; +} diff --git a/src/MindControl.Code/Results/HookFailure.cs b/src/MindControl.Code/Results/HookFailure.cs new file mode 100644 index 0000000..fd8ad31 --- /dev/null +++ b/src/MindControl.Code/Results/HookFailure.cs @@ -0,0 +1,151 @@ +using Iced.Intel; + +namespace MindControl.Results; + +/// +/// Enumerates the possible reasons for a hook operation to fail. +/// +public enum HookFailureReason +{ + /// + /// The given pointer path could not be successfully evaluated. + /// + PathEvaluationFailure, + + /// + /// The arguments provided to the hook operation are invalid. + /// + InvalidArguments, + + /// + /// The memory allocation operation failed. + /// + AllocationFailure, + + /// + /// A reading operation failed. + /// + ReadFailure, + + /// + /// A code disassembling operation failed. + /// + DecodingFailure, + + /// + /// Instructions could not be assembled into a code block. + /// + CodeAssemblyFailure, + + /// + /// A write operation failed. + /// + WriteFailure +} + +/// +/// Represents a failure that occurred in a hook operation. +/// +/// Reason for the failure. +public abstract record HookFailure(HookFailureReason Reason); + +/// +/// Represents a failure that occurred in a hook operation when the pointer path failed to evaluate. +/// +/// Details about the path evaluation failure. +public record HookFailureOnPathEvaluation(PathEvaluationFailure Details) + : HookFailure(HookFailureReason.PathEvaluationFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failed to evaluate the given pointer path: {Details}"; +} + +/// +/// Represents a failure that occurred in a hook operation when the arguments provided are invalid. +/// +/// Message that describes how the arguments fail to meet expectations. +public record HookFailureOnInvalidArguments(string Message) + : HookFailure(HookFailureReason.InvalidArguments) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The arguments provided are invalid: {Message}"; +} + +/// +/// Represents a failure that occurred in a hook operation when the memory allocation operation required to store the +/// injected code failed. +/// +/// Details about the allocation failure. +public record HookFailureOnAllocationFailure(AllocationFailure Details) + : HookFailure(HookFailureReason.AllocationFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failed to allocate memory for the injected code: {Details}"; +} + +/// +/// Represents a failure that occurred in a hook operation when a reading operation failed. +/// +/// Details about the read failure. +public record HookFailureOnReadFailure(ReadFailure Details) + : HookFailure(HookFailureReason.ReadFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"A reading operation failed: {Details}"; +} + +/// +/// Represents a failure that occurred in a hook operation when a code disassembling operation failed. +/// +/// Error code that describes the failure. +public record HookFailureOnDecodingFailure(DecoderError Error) + : HookFailure(HookFailureReason.DecodingFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failed to decode instructions with the following error code: {Error}"; +} + +/// Enumerates potential blocks for a . +public enum HookCodeAssemblySource +{ + /// Default value used when the source is unknown. + Unkown, + /// Designates the jump instruction that forwards execution to the injected code. + JumpToInjectedCode, + /// Designates the code block that is prepended to the injected code. + PrependedCode, + /// Designates the injected code block itself. + InjectedCode, + /// Designates the code block that is appended to the injected code. + AppendedCode +} + +/// +/// Represents a failure that occurred in a hook operation when instructions could not be assembled into a code block. +/// +/// Block where the code assembly failed. +/// Message that describes the failure. +public record HookFailureOnCodeAssembly(HookCodeAssemblySource Source, string Message) + : HookFailure(HookFailureReason.CodeAssemblyFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failed to assemble code: {Message}"; +} + +/// +/// Represents a failure that occurred in a hook operation when a write operation failed. +/// +/// Details about the write failure. +public record HookFailureOnWriteFailure(WriteFailure Details) + : HookFailure(HookFailureReason.WriteFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"A write operation failed: {Details}"; +} diff --git a/src/MindControl/Addressing/MemoryRange.cs b/src/MindControl/Addressing/MemoryRange.cs index 8d8be83..b3b852c 100644 --- a/src/MindControl/Addressing/MemoryRange.cs +++ b/src/MindControl/Addressing/MemoryRange.cs @@ -10,7 +10,12 @@ public readonly record struct MemoryRange /// End address of the range. public UIntPtr End { get; init; } - + + /// Instance representing the full 32-bits address space. + /// There is no 64-bits equivalent because, if the system is 32-bits, such a range cannot be represented. + /// + public static readonly MemoryRange Full32BitsRange = new(UIntPtr.Zero, (UIntPtr)int.MaxValue); + /// /// Builds a . /// @@ -43,7 +48,7 @@ public static MemoryRange FromStartAndSize(UIntPtr start, ulong size) /// /// Address to check. /// True if the address is within the memory range, false otherwise. - public bool IsInRange(UIntPtr address) + public bool Contains(UIntPtr address) => address.ToUInt64() >= Start.ToUInt64() && address.ToUInt64() <= End.ToUInt64(); /// @@ -54,6 +59,27 @@ public bool IsInRange(UIntPtr address) public bool Contains(MemoryRange range) => range.Start.ToUInt64() >= Start.ToUInt64() && range.End.ToUInt64() <= End.ToUInt64(); + /// + /// Determines the shortest distance between the specified address and any address within the range. + /// + /// Target address. + /// The distance between the address and the range. + public ulong DistanceTo(UIntPtr address) + { + ulong addressValue = address.ToUInt64(); + ulong startValue = Start.ToUInt64(); + + if (addressValue < startValue) + return startValue - addressValue; + + ulong endValue = End.ToUInt64(); + if (addressValue > endValue) + return addressValue - endValue; + + // Address is within the range + return 0; + } + /// /// Determines if the specified range overlaps with this range. /// diff --git a/src/MindControl/Addressing/PointerExtensions.cs b/src/MindControl/Addressing/PointerExtensions.cs new file mode 100644 index 0000000..15ab048 --- /dev/null +++ b/src/MindControl/Addressing/PointerExtensions.cs @@ -0,0 +1,43 @@ +namespace MindControl; + +/// +/// Provides extension methods for pointers. +/// +public static class PointerExtensions +{ + /// + /// Determines the distance between two pointers, without wrap-around. + /// + /// First pointer. + /// Second pointer. + /// The distance between the two pointers, without wrap-around. + /// + /// As remarked by the documentation, the distance provided does not wrap around, meaning that, for example, the + /// distance between 0 and is equal to and not 1. + /// + public static ulong DistanceTo(this UIntPtr a, UIntPtr b) + { + ulong aValue = a.ToUInt64(); + ulong bValue = b.ToUInt64(); + return aValue < bValue ? bValue - aValue : aValue - bValue; + } + + /// + /// Gets a range of memory around the given address, with the specified size and without wrap-around. + /// + /// Target address. + /// Size of the range. Note that the resulting range may be smaller if the address is near the + /// beginning or end of the address space. + /// A memory range around the address, with the specified size and without wrap-around. + public static MemoryRange GetRangeAround(this UIntPtr address, ulong size) + { + if (size < 2) + throw new ArgumentException("The size must be at least 2 bytes.", nameof(size)); + + ulong addressValue = address.ToUInt64(); + ulong halfSize = size / 2; + ulong start = addressValue <= halfSize ? 0 : addressValue - halfSize; + ulong end = ulong.MaxValue - addressValue <= halfSize ? ulong.MaxValue : addressValue + halfSize; + return new MemoryRange(new UIntPtr(start), new UIntPtr(end)); + } +} \ No newline at end of file diff --git a/src/MindControl/Addressing/ProcessMemoryStream.cs b/src/MindControl/Addressing/ProcessMemoryStream.cs index 8de40b9..ac5e01f 100644 --- a/src/MindControl/Addressing/ProcessMemoryStream.cs +++ b/src/MindControl/Addressing/ProcessMemoryStream.cs @@ -150,18 +150,14 @@ public override void Write(byte[] buffer, int offset, int count) throw new ArgumentException("The buffer is too small to write the requested number of bytes.", nameof(buffer)); - var result = _osService.WriteProcessMemoryPartial(_processHandle, _position, buffer, offset, count); + var result = _osService.WriteProcessMemory(_processHandle, _position, buffer.AsSpan(offset, count)); - // Unlike Read, we will throw an exception if the write operation failed. - // This is because write operations are expected to fully complete. + // Unlike Read, we will throw an exception if the write operation failed, because write operations are expected + // to fully complete. if (result.IsFailure) throw new IOException("Failed to write into the process memory.", result.ToException()); - ulong written = result.Value; - if (written != (ulong)count) - throw new IOException($"Failed to write all bytes into the process memory. Only {written} bytes were written (expected {count})."); - - _position = (UIntPtr)(_position.ToUInt64() + written); + _position += (UIntPtr)count; } } \ No newline at end of file diff --git a/src/MindControl/Allocation/MemoryReservation.cs b/src/MindControl/Allocation/MemoryReservation.cs index f94a6d5..4a049a4 100644 --- a/src/MindControl/Allocation/MemoryReservation.cs +++ b/src/MindControl/Allocation/MemoryReservation.cs @@ -27,7 +27,13 @@ public class MemoryReservation /// Gets a boolean indicating if the reservation has been disposed. /// public bool IsDisposed { get; private set; } - + + /// + /// Gets the size of the reservation in bytes. This is a shortcut for calling on + /// the . + /// + public ulong Size => Range.GetSize(); + /// /// Builds a new instance. /// diff --git a/src/MindControl/MindControl.csproj b/src/MindControl/MindControl.csproj index 36aee27..d6f37ee 100644 --- a/src/MindControl/MindControl.csproj +++ b/src/MindControl/MindControl.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable MindControl diff --git a/src/MindControl/Native/IOperatingSystemService.cs b/src/MindControl/Native/IOperatingSystemService.cs index 096ce10..3c09fff 100644 --- a/src/MindControl/Native/IOperatingSystemService.cs +++ b/src/MindControl/Native/IOperatingSystemService.cs @@ -69,31 +69,9 @@ Result ReadAndOverwriteProtection(IntPtr proces /// Handle of the target process. The handle must have PROCESS_VM_WRITE and /// PROCESS_VM_OPERATION access. /// Base address in the memory of the process to which data will be written. - /// Byte array to write in the memory. It is assumed that the entire array will be - /// written, unless a size is specified. - /// Specify this value if you only want to write part of the value array in memory. - /// This parameter is useful when using buffer byte arrays. Leave it to null to use the entire array. + /// Bytes to write in the process memory. /// A result indicating either a success or a system failure. - Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, byte[] value, - int? size = null); - - /// - /// Writes the given bytes into the memory of the specified process, at the target address. Supports partial reads, - /// in case the full length failed to be written but at least one byte was successfully written. - /// Prefer in most cases. - /// - /// Handle of the target process. The handle must have PROCESS_VM_WRITE and - /// PROCESS_VM_OPERATION access. - /// Base address in the memory of the process to which data will be written. - /// Byte array to write in the memory. Depending on the and - /// parameters, only part of the buffer may be copied into the process memory. - /// Offset in the buffer where the data to write starts. - /// Number of bytes to write from the buffer into the process memory, starting from the - /// . - /// A result holding either the number of bytes written, or a system failure when no bytes were written. - /// - Result WriteProcessMemoryPartial(IntPtr processHandle, UIntPtr targetAddress, byte[] buffer, - int offset, int size); + Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, Span value); /// /// Allocates memory in the specified process. The address is determined automatically by the operating system. diff --git a/src/MindControl/Native/Win32Service.Imports.cs b/src/MindControl/Native/Win32Service.Imports.cs index df61801..8df110a 100644 --- a/src/MindControl/Native/Win32Service.Imports.cs +++ b/src/MindControl/Native/Win32Service.Imports.cs @@ -463,26 +463,6 @@ public static extern bool VirtualProtectEx(IntPtr hProcess, UIntPtr lpAddress, /// the specified process. This parameter is optional. If null, it will be ignored. /// If the function succeeds, the return value is true. Otherwise, it will be false. [DllImport("kernel32.dll", SetLastError = true)] - public static extern bool WriteProcessMemory(IntPtr hProcess, UIntPtr lpBaseAddress, byte[] lpBuffer, UIntPtr nSize, - out UIntPtr lpNumberOfBytesWritten); - - /// - /// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible - /// or the operation fails. This variant uses a pointer for the buffer, which allows callers to avoid unnecessary - /// memory allocations in certain cases. - /// - /// A handle to the process memory to be modified. The handle must have PROCESS_VM_WRITE and - /// PROCESS_VM_OPERATION access to the process. - /// A pointer to the base address in the specified process to which data is written. - /// Before data transfer occurs, the system verifies that all data in the base address and memory of the specified - /// size is accessible for write access, and if it is not accessible, the function fails. - /// A pointer to the buffer that contains data to be written in the address space of the - /// specified process. - /// The number of bytes to be written to the specified process. - /// A pointer to a variable that receives the number of bytes transferred into - /// the specified process. This parameter is optional. If null, it will be ignored. - /// If the function succeeds, the return value is true. Otherwise, it will be false. - [DllImport("kernel32.dll", SetLastError = true)] - public static extern bool WriteProcessMemory(IntPtr hProcess, UIntPtr lpBaseAddress, UIntPtr lpBuffer, + public static extern bool WriteProcessMemory(IntPtr hProcess, UIntPtr lpBaseAddress, ref byte lpBuffer, UIntPtr nSize, out UIntPtr lpNumberOfBytesWritten); } \ No newline at end of file diff --git a/src/MindControl/Native/Win32Service.cs b/src/MindControl/Native/Win32Service.cs index 66629dc..134e2d8 100644 --- a/src/MindControl/Native/Win32Service.cs +++ b/src/MindControl/Native/Win32Service.cs @@ -87,7 +87,8 @@ public Result ReadProcessMemory(IntPtr processHandle, UIn /// /// Reads a targeted range of the memory of a specified process into the given buffer. Supports partial reads, in /// case the full length failed to be read but at least one byte was successfully copied into the buffer. - /// Prefer when you know the length of the data to read. + /// Prefer when you know the length of the data + /// to read. /// /// Handle of the target process. The handle must have PROCESS_VM_READ access. /// Starting address of the memory range to read. @@ -255,12 +256,9 @@ public Result ReadAndOverwriteProtection(IntPtr /// Handle of the target process. The handle must have PROCESS_VM_WRITE and /// PROCESS_VM_OPERATION access. /// Base address in the memory of the process to which data will be written. - /// Byte array to write in the memory. It is assumed that the entire array will be - /// written, unless a size is specified. - /// Specify this value if you only want to write part of the value array in memory. - /// This parameter is useful when using buffer byte arrays. Leave it to null to use the entire array. + /// Bytes to write in the process memory. /// A result indicating either a success or a system failure. - public Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, byte[] value, int? size = null) + public Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, Span value) { if (processHandle == IntPtr.Zero) return new SystemFailureOnInvalidArgument(nameof(processHandle), @@ -268,74 +266,11 @@ public Result WriteProcessMemory(IntPtr processHandle, UIntPtr ta if (targetAddress == UIntPtr.Zero) return new SystemFailureOnInvalidArgument(nameof(targetAddress), "The target address cannot be a zero pointer."); - if (size != null && size.Value > value.Length) - return new SystemFailureOnInvalidArgument(nameof(size), - "The size cannot exceed the length of the value array."); - - var result = WriteProcessMemory(processHandle, targetAddress, value, (UIntPtr)(size ?? value.Length), - out _); - - return result ? Result.Success : GetLastSystemError(); - } - - /// - /// Writes the given bytes into the memory of the specified process, at the target address. Supports partial reads, - /// in case the full length failed to be written but at least one byte was successfully written. - /// Prefer in most cases. - /// - /// Handle of the target process. The handle must have PROCESS_VM_WRITE and - /// PROCESS_VM_OPERATION access. - /// Base address in the memory of the process to which data will be written. - /// Byte array to write in the memory. Depending on the and - /// parameters, only part of the buffer may be copied into the process memory. - /// Offset in the buffer where the data to write starts. - /// Number of bytes to write from the buffer into the process memory, starting from the - /// . - /// A result holding either the number of bytes written, or a system failure when no bytes were written. - /// - public Result WriteProcessMemoryPartial(IntPtr processHandle, UIntPtr targetAddress, - byte[] buffer, int offset, int size) - { - if (processHandle == IntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(processHandle), - "The process handle is invalid (zero pointer)."); - if (buffer.Length < offset + size) - return new SystemFailureOnInvalidArgument(nameof(buffer), - "The buffer is too small to write the requested number of bytes."); - // We need to take in account the offset, meaning we can only copy from the buffer from a certain position, - // defined as the "offset" parameter. - // This is a problem, because the Win32 API doesn't have that. It starts copying from the beginning of whatever - // buffer you pass in. - // But in fact, as with any array, the Win32 API sees the buffer as a pointer. This means we can use an - // alternative signature that uses a UIntPtr as the buffer instead of a byte array. - // Which, in turns, means that we can call it with the address of a specific element in the buffer, and the API - // will start copying from there. + var result = WriteProcessMemory(processHandle, targetAddress, ref value.GetPinnableReference(), + (UIntPtr)value.Length, out _); - // To get the pointer to the right element in the buffer, we first have to pin the buffer in memory. - // This ensures that the garbage collector doesn't move the buffer around while we're working with it. - var bufferGcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); - try - { - // Then we use a Marshal method to get a pointer to the element in the buffer at the given offset. - var bufferPtr = (UIntPtr)Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset).ToInt64(); - - // Finally, we can call the Win32 API with the pointer to the right element in the buffer. - bool returnValue = WriteProcessMemory(processHandle, targetAddress, bufferPtr, (UIntPtr)size, - out var bytesWritten); - - // Only return a failure when the function failed AND didn't write anything. - // This ensures that a non-error result is returned for a partial write. - if (bytesWritten == UIntPtr.Zero && !returnValue) - return GetLastSystemError(); - - return bytesWritten.ToUInt64(); - } - finally - { - // After using the pinned buffer, we must free it, so that the garbage collector can handle it again. - bufferGcHandle.Free(); - } + return result ? Result.Success : GetLastSystemError(); } /// diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs index c9a71d3..6bb09e4 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs @@ -14,7 +14,7 @@ public partial class ProcessMemory /// The memory address pointed by the pointer path. public Result EvaluateMemoryAddress(PointerPath pointerPath) { - if (pointerPath.IsStrictly64Bits && (IntPtr.Size == 4 || !_is64Bits)) + if (pointerPath.IsStrictly64Bits && (IntPtr.Size == 4 || !Is64Bits)) return new PathEvaluationFailureOnIncompatibleBitness(); UIntPtr? baseAddress; @@ -100,7 +100,7 @@ public Result GetMemoryStream(Pointe /// Starting address of the stream. /// The created process memory stream. public ProcessMemoryStream GetMemoryStream(UIntPtr startAddress) - => new ProcessMemoryStream(_osService, _processHandle, startAddress); + => new(_osService, ProcessHandle, startAddress); /// /// Gets the process module with the given name. diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index 2d988f9..cac6e25 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -17,21 +17,22 @@ public partial class ProcessMemory /// /// Attempts to allocate a memory range of the given size within the process. Use this method only when automatic - /// allocation management through the Store methods is not appropriate. + /// allocation management through the Store methods or method are not appropriate. /// /// Size of the memory range to allocate. /// Determines if the memory range can be used to store executable code. /// Specify this parameter to limit the allocation to a specific range of memory. + /// If specified, try to allocate as close as possible to this address. /// A result holding either the allocated memory range, or an allocation failure. public Result Allocate(ulong size, bool forExecutableCode, - MemoryRange? limitRange = null) + MemoryRange? limitRange = null, UIntPtr? nearAddress = null) { if (size == 0) - throw new ArgumentException("The size of the memory range to allocate must be greater than zero.", - nameof(size)); + return new AllocationFailureOnInvalidArguments( + "The size of the memory range to allocate must be greater than zero."); // Find a free memory range that satisfies the size needed - var rangeResult = FindAndAllocateFreeMemory(size, forExecutableCode, limitRange); + var rangeResult = FindAndAllocateFreeMemory(size, forExecutableCode, limitRange, nearAddress); if (rangeResult.IsFailure) return rangeResult.Error; @@ -53,7 +54,7 @@ internal void Free(MemoryAllocation allocation) return; _allocations.Remove(allocation); - _osService.ReleaseMemory(_processHandle, allocation.Range.Start); + _osService.ReleaseMemory(ProcessHandle, allocation.Range.Start); } /// @@ -63,11 +64,12 @@ internal void Free(MemoryAllocation allocation) /// Set to true to allocate a memory block with execute permissions. /// Specify this parameter to limit the search to a specific range of memory. /// If left null (default), the entire process memory will be searched. + /// If specified, try to allocate as close as possible to this address. /// A result holding either the memory range found, or an allocation failure. /// The reason why the method performs the allocation itself is because we cannot know if the range can /// actually be allocated without performing the allocation. private Result FindAndAllocateFreeMemory(ulong sizeNeeded, - bool forExecutableCode, MemoryRange? limitRange = null) + bool forExecutableCode, MemoryRange? limitRange = null, UIntPtr? nearAddress = null) { var maxRange = _osService.GetFullMemoryRange(); var actualRange = limitRange == null ? maxRange : maxRange.Intersect(limitRange.Value); @@ -83,28 +85,59 @@ private Result FindAndAllocateFreeMemory(ulong s uint minFittingPageSize = (uint)(sizeNeeded / pageSize + (isDirectMultiple ? (ulong)0 : 1)) * pageSize; // Browse through regions in the memory range to find the first one that satisfies the size needed - var nextAddress = actualRange.Value.Start; + // If a near address is specified, start there. Otherwise, start at the beginning of the memory range. + // For near address search, we are going to search back and forth around the address, which complicates the + // process a bit. It means we have to keep track of both the next lowest address and next highest address. + var nextAddress = nearAddress ?? actualRange.Value.Start; + var nextAddressForward = nextAddress; + var nextAddressBackwards = nextAddress; + bool goingForward = true; + MemoryRange? freeRange = null; MemoryRangeMetadata currentMetadata; - while (nextAddress.ToUInt64() < actualRange.Value.End.ToUInt64() - && (currentMetadata = _osService.GetRegionMetadata(_processHandle, nextAddress, _is64Bits) + while ((nextAddressForward.ToUInt64() < actualRange.Value.End.ToUInt64() + || nextAddressBackwards.ToUInt64() > actualRange.Value.Start.ToUInt64()) + && (currentMetadata = _osService.GetRegionMetadata(ProcessHandle, nextAddress, Is64Bits) .GetValueOrDefault()).Size.ToUInt64() > 0) { - nextAddress = (UIntPtr)(nextAddress.ToUInt64() + currentMetadata.Size.ToUInt64()); - + nextAddressForward = (UIntPtr)Math.Max(nextAddressForward.ToUInt64(), + nextAddress.ToUInt64() + currentMetadata.Size.ToUInt64()); + nextAddressBackwards = (UIntPtr)Math.Min(nextAddressBackwards.ToUInt64(), + currentMetadata.StartAddress.ToUInt64() - 1); + + nextAddress = goingForward ? nextAddressForward : nextAddressBackwards; + // If the current region cannot be used, reinitialize the current free range and keep iterating if (!currentMetadata.IsFree) { freeRange = null; + + // In a near address search, we may change direction there depending on which next address is closest. + if (nearAddress != null) + { + var forwardDistance = nearAddress.Value.DistanceTo(nextAddressForward); + var backwardDistance = nearAddress.Value.DistanceTo(nextAddressBackwards); + goingForward = forwardDistance <= backwardDistance; + nextAddress = goingForward ? nextAddressForward : nextAddressBackwards; + } + continue; } - - // Build a range with the current region - // Start from the start of the current free range if it's not null, so that we can have ranges that span - // across multiple regions. - freeRange = new MemoryRange(freeRange?.Start ?? currentMetadata.StartAddress, - (UIntPtr)(currentMetadata.StartAddress.ToUInt64() + currentMetadata.Size.ToUInt64())); + // Build a range with the current region + // Extend the free range if it's not null, so that we can have ranges that span across multiple regions. + if (goingForward) + { + freeRange = new MemoryRange(freeRange?.Start ?? currentMetadata.StartAddress, + (UIntPtr)(currentMetadata.StartAddress.ToUInt64() + currentMetadata.Size.ToUInt64())); + } + else + { + freeRange = new MemoryRange(currentMetadata.StartAddress, + (UIntPtr)(freeRange?.End.ToUInt64() ?? currentMetadata.StartAddress.ToUInt64() + + currentMetadata.Size.ToUInt64())); + } + if (freeRange.Value.GetSize() >= sizeNeeded) { // The free range is large enough. @@ -115,7 +148,7 @@ private Result FindAndAllocateFreeMemory(ulong s // Even if they are free, some regions cannot be allocated. // The only way to know if a region can be allocated is to try to allocate it. - var allocateResult = _osService.AllocateMemory(_processHandle, finalRange.Start, + var allocateResult = _osService.AllocateMemory(ProcessHandle, finalRange.Start, (int)finalRange.GetSize(), MemoryAllocationType.Commit | MemoryAllocationType.Reserve, forExecutableCode ? MemoryProtection.ExecuteReadWrite : MemoryProtection.ReadWrite); @@ -125,6 +158,16 @@ private Result FindAndAllocateFreeMemory(ulong s // The allocation failed. Reset the current range and keep iterating. freeRange = null; + + // In a near address search, we may change direction there depending on which next address is closest. + if (nearAddress != null) + { + var forwardDistance = nearAddress.Value.DistanceTo(nextAddressForward); + var backwardDistance = nearAddress.Value.DistanceTo(nextAddressBackwards); + goingForward = forwardDistance <= backwardDistance; + nextAddress = goingForward ? nextAddressForward : nextAddressBackwards; + } + continue; } } @@ -140,13 +183,32 @@ private Result FindAndAllocateFreeMemory(ulong s /// a new range is allocated, and a reservation is made on it. /// /// Size of the memory range to reserve. - /// Set to true if the memory range must be executable. + /// Set to true if the memory range must be executable (to store code). + /// Specify this parameter to limit the reservation to allocations within a specific range + /// of memory. If left null (default), any allocation can be used. Otherwise, only allocations within the specified + /// range will be considered, and if none are available, a new allocation will be attempted within that range. + /// + /// If specified, prioritize allocations by their proximity to this address. If no + /// matching allocation is found, a new allocation as close as possible to this address will be attempted. /// A result holding either the resulting reservation, or an allocation failure. - private Result FindOrMakeReservation(ulong size, bool requireExecutable) + public Result Reserve(ulong size, bool requireExecutable, + MemoryRange? limitRange = null, UIntPtr? nearAddress = null) { - uint alignment = _is64Bits ? (uint)8 : 4; - var reservationInExistingAllocation = _allocations - .Where(a => !requireExecutable || a.IsExecutable) + if (size == 0) + return new AllocationFailureOnInvalidArguments( + "The size of the memory range to reserve must be greater than zero."); + + uint alignment = Is64Bits ? (uint)8 : 4; + var existingAllocations = _allocations + .Where(a => (!requireExecutable || a.IsExecutable) + && (limitRange == null || limitRange.Value.Contains(a.Range.Start))); + + // If we have a near address, sort the allocations by their distance to that address + if (nearAddress != null) + existingAllocations = existingAllocations.OrderBy(a => a.Range.DistanceTo(nearAddress.Value)); + + // Pick the first allocation that works + var reservationInExistingAllocation = existingAllocations .Select(r => r.ReserveRange(size, alignment)) .FirstOrDefault(r => r.IsSuccess) ?.Value; @@ -156,7 +218,7 @@ private Result FindOrMakeReservation(ulong return reservationInExistingAllocation; // No allocation could satisfy the reservation: allocate a new range - var allocationResult = Allocate(size, requireExecutable); + var allocationResult = Allocate(size, requireExecutable, limitRange, nearAddress); if (allocationResult.IsFailure) return allocationResult.Error; @@ -183,7 +245,7 @@ private Result FindOrMakeReservation(ulong /// A result holding either the reserved memory range, or an allocation failure. public Result Store(byte[] data, bool isCode = false) { - var reservedRangeResult = FindOrMakeReservation((ulong)data.Length, isCode); + var reservedRangeResult = Reserve((ulong)data.Length, isCode); if (reservedRangeResult.IsFailure) return reservedRangeResult.Error; @@ -203,7 +265,7 @@ public Result Store(byte[] data, bool isCo /// A result holding either the reservation storing the data, or a reservation failure. public Result Store(byte[] data, MemoryAllocation allocation) { - uint alignment = _is64Bits ? (uint)8 : 4; + uint alignment = Is64Bits ? (uint)8 : 4; var reservedRangeResult = allocation.ReserveRange((ulong)data.Length, alignment); if (reservedRangeResult.IsFailure) return reservedRangeResult.Error; diff --git a/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs b/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs index 02ab750..74dae3d 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs @@ -35,14 +35,14 @@ public Result InjectLibrary(string libraryPath) // Write the library path string into the process memory var libraryPathBytes = Encoding.Unicode.GetBytes(absoluteLibraryPath); - var allocateStringResult = _osService.AllocateMemory(_processHandle, libraryPathBytes.Length + 1, + var allocateStringResult = _osService.AllocateMemory(ProcessHandle, libraryPathBytes.Length + 1, MemoryAllocationType.Commit | MemoryAllocationType.Reserve, MemoryProtection.ReadWrite); if (allocateStringResult.IsFailure) return new InjectionFailureOnSystemFailure("Could not allocate memory to store the library file path.", allocateStringResult.Error); var allocatedLibPathAddress = allocateStringResult.Value; - var writeStringResult = _osService.WriteProcessMemory(_processHandle, allocatedLibPathAddress, + var writeStringResult = _osService.WriteProcessMemory(ProcessHandle, allocatedLibPathAddress, libraryPathBytes); if (writeStringResult.IsFailure) return new InjectionFailureOnSystemFailure( @@ -58,7 +58,7 @@ public Result InjectLibrary(string libraryPath) loadLibraryAddressResult.Error); var loadLibraryFunctionAddress = loadLibraryAddressResult.Value; - var threadHandleResult = _osService.CreateRemoteThread(_processHandle, loadLibraryFunctionAddress, + var threadHandleResult = _osService.CreateRemoteThread(ProcessHandle, loadLibraryFunctionAddress, allocatedLibPathAddress); if (threadHandleResult.IsFailure) return new InjectionFailureOnSystemFailure( @@ -76,7 +76,7 @@ public Result InjectLibrary(string libraryPath) return new InjectionFailureOnTimeout(); // Free the memory used for the library path string - _osService.ReleaseMemory(_processHandle, allocatedLibPathAddress); + _osService.ReleaseMemory(ProcessHandle, allocatedLibPathAddress); // Close the thread handle _osService.CloseHandle(threadHandle); diff --git a/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs b/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs index d6a1693..3986066 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs @@ -136,7 +136,7 @@ private IEnumerable FindBytesInternal(ByteSearchPattern pattern, Memory private IEnumerable ScanRangeForBytePattern(byte[] bytePattern, byte[] mask, MemoryRange range) { // Read the whole memory range and place it in a byte array. - byte[] rangeMemory = _osService.ReadProcessMemory(_processHandle, range.Start, range.GetSize()) + byte[] rangeMemory = _osService.ReadProcessMemory(ProcessHandle, range.Start, range.GetSize()) .GetValueOrDefault() ?? Array.Empty(); int maxIndex = rangeMemory.Length - bytePattern.Length; @@ -182,7 +182,7 @@ private MemoryRange[] GetAggregatedRegionRanges(MemoryRange range, FindBytesSett UIntPtr currentAddress = range.Start; while (currentAddress.ToUInt64() <= rangeEnd) { - var getRegionResult = _osService.GetRegionMetadata(_processHandle, currentAddress, _is64Bits); + var getRegionResult = _osService.GetRegionMetadata(ProcessHandle, currentAddress, Is64Bits); if (getRegionResult.IsFailure) { // If we failed to get the region metadata, we cannot continue because we don't know where the next diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs index 5612805..37cefd1 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs @@ -72,7 +72,7 @@ public Result ReadBytes(UIntPtr address, ulong length) if (!IsBitnessCompatible(address)) return new ReadFailureOnIncompatibleBitness(address); - var readResult = _osService.ReadProcessMemory(_processHandle, address, length); + var readResult = _osService.ReadProcessMemory(ProcessHandle, address, length); return readResult.IsSuccess ? readResult.Value : new ReadFailureOnSystemRead(readResult.Error); } @@ -117,7 +117,7 @@ public Result ReadBytesPartial(UIntPtr address, byte[] buffe if (!IsBitnessCompatible(address)) return new ReadFailureOnIncompatibleBitness(address); - var readResult = _osService.ReadProcessMemoryPartial(_processHandle, address, buffer, 0, maxLength); + var readResult = _osService.ReadProcessMemoryPartial(ProcessHandle, address, buffer, 0, maxLength); return readResult.IsSuccess ? readResult.Value : new ReadFailureOnSystemRead(readResult.Error); } @@ -170,7 +170,7 @@ public Result Read(UIntPtr address) where T : struct } // Read the bytes from the process memory - var readResult = _osService.ReadProcessMemory(_processHandle, address, (ulong)size); + var readResult = _osService.ReadProcessMemory(ProcessHandle, address, (ulong)size); if (readResult.IsFailure) return new ReadFailureOnSystemRead(readResult.Error); byte[] bytes = readResult.Value; @@ -226,7 +226,7 @@ public Result Read(Type type, UIntPtr address) int size = Marshal.SizeOf(type); // Read the bytes from the process memory - var readResult = _osService.ReadProcessMemory(_processHandle, address, (ulong)size); + var readResult = _osService.ReadProcessMemory(ProcessHandle, address, (ulong)size); if (!readResult.IsSuccess) return new ReadFailureOnSystemRead(readResult.Error); @@ -648,7 +648,7 @@ private Result ReadStringWithLengthPrefixInCharacters int stringBytesOffset = lengthPrefixOffset + settings.LengthPrefix.Size; int bufferSize = Math.Min(settings.Encoding.GetMaxByteCount((int)expectedStringLength), StringReadingBufferMaxSize); - using var stream = GetMemoryStream(address + stringBytesOffset); + using var stream = GetMemoryStream(address + (UIntPtr)stringBytesOffset); using var streamReader = new StreamReader(stream, settings.Encoding, false, bufferSize, false); // Read characters until we reach the expected length @@ -688,7 +688,7 @@ private Result ReadStringWithNullTerminator(UIntPtr a // Get a memory stream after the prefixes int stringBytesOffset = (settings.TypePrefix?.Length ?? 0) + (settings.LengthPrefix?.Size ?? 0); - using var stream = GetMemoryStream(address + stringBytesOffset); + using var stream = GetMemoryStream(address + (UIntPtr)stringBytesOffset); using var streamReader = new StreamReader(stream, settings.Encoding, false, StringReadingBufferDefaultSize, false); diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs index 5eca263..67c3823 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs @@ -33,7 +33,7 @@ public Result WriteBytes(PointerPath path, byte[] value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// A successful result, or a write failure - public Result WriteBytes(UIntPtr address, byte[] value, + public Result WriteBytes(UIntPtr address, Span value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { // Remove protection if needed @@ -41,7 +41,7 @@ public Result WriteBytes(UIntPtr address, byte[] value, MemoryProtection? previousProtection = null; if (memoryProtectionStrategy is MemoryProtectionStrategy.Remove or MemoryProtectionStrategy.RemoveAndRestore) { - var protectionRemovalResult = _osService.ReadAndOverwriteProtection(_processHandle, _is64Bits, + var protectionRemovalResult = _osService.ReadAndOverwriteProtection(ProcessHandle, Is64Bits, address, MemoryProtection.ExecuteReadWrite); if (protectionRemovalResult.IsFailure) @@ -51,7 +51,7 @@ public Result WriteBytes(UIntPtr address, byte[] value, } // Write memory - var writeResult = _osService.WriteProcessMemory(_processHandle, address, value); + var writeResult = _osService.WriteProcessMemory(ProcessHandle, address, value); if (writeResult.IsFailure) return new WriteFailureOnSystemWrite(address, writeResult.Error); @@ -59,7 +59,7 @@ public Result WriteBytes(UIntPtr address, byte[] value, if (memoryProtectionStrategy == MemoryProtectionStrategy.RemoveAndRestore && previousProtection != MemoryProtection.ExecuteReadWrite) { - var protectionRestorationResult = _osService.ReadAndOverwriteProtection(_processHandle, _is64Bits, + var protectionRestorationResult = _osService.ReadAndOverwriteProtection(ProcessHandle, Is64Bits, address, previousProtection!.Value); if (protectionRestorationResult.IsFailure) @@ -208,7 +208,7 @@ private Result WriteUInt(UIntPtr address, uint value, /// A successful result, or a write failure private Result WriteIntPtr(UIntPtr address, IntPtr value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteBytes(address, value.ToBytes(_is64Bits), memoryProtectionStrategy); + => WriteBytes(address, value.ToBytes(Is64Bits), memoryProtectionStrategy); /// /// Writes a float to the given address in the process memory. diff --git a/src/MindControl/ProcessMemory/ProcessMemory.cs b/src/MindControl/ProcessMemory/ProcessMemory.cs index c46aa5e..cc504b7 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.cs @@ -18,14 +18,23 @@ public partial class ProcessMemory : IDisposable private readonly Process _process; private readonly IOperatingSystemService _osService; - private IntPtr _processHandle; - private bool _is64Bits; private readonly bool _ownsProcessInstance; /// /// Gets a value indicating if the process is currently attached or not. /// public bool IsAttached { get; private set; } + + /// + /// Gets a boolean indicating if the process is 64-bits. + /// + public bool Is64Bits { get; private set; } + + /// + /// Gets the handle of the attached process. + /// Use this if you need to manually call Win32 API functions. + /// + public IntPtr ProcessHandle { get; private set; } /// /// Gets or sets the default way this instance deals with memory protection. @@ -130,16 +139,16 @@ private void Attach() { try { - _is64Bits = _osService.IsProcess64Bits(_process.Id).GetValueOrDefault(defaultValue: true); + Is64Bits = _osService.IsProcess64Bits(_process.Id).GetValueOrDefault(defaultValue: true); - if (_is64Bits && !Environment.Is64BitOperatingSystem) + if (Is64Bits && !Environment.Is64BitOperatingSystem) throw new ProcessException(_process.Id, "A 32-bit program cannot attach to a 64-bit process."); _process.EnableRaisingEvents = true; _process.Exited += OnProcessExited; var openResult = _osService.OpenProcess(_process.Id); openResult.ThrowOnError(); - _processHandle = openResult.Value; + ProcessHandle = openResult.Value; IsAttached = true; } @@ -155,7 +164,7 @@ private void Attach() /// In other words, returns false if the pointer is a 64-bit address but the target process is 32-bit. /// /// Pointer to test. - private bool IsBitnessCompatible(UIntPtr pointer) => _is64Bits || pointer.ToUInt64() <= uint.MaxValue; + private bool IsBitnessCompatible(UIntPtr pointer) => Is64Bits || pointer.ToUInt64() <= uint.MaxValue; /// /// Gets a new instance of representing the attached process. @@ -173,19 +182,6 @@ private void Detach() if (IsAttached) { IsAttached = false; - for (int i = _allocations.Count - 1; i >= 0; i--) - { - try - { - _allocations[i].Dispose(); - } - catch (Exception) - { - // Just skip. We probably lost the process and thus cannot do anything with it anymore. - _allocations.RemoveAt(i); - } - } - _process.Exited -= OnProcessExited; ProcessDetached?.Invoke(this, EventArgs.Empty); } diff --git a/src/MindControl/Results/Result.cs b/src/MindControl/Results/Result.cs index 77136c4..8871667 100644 --- a/src/MindControl/Results/Result.cs +++ b/src/MindControl/Results/Result.cs @@ -21,7 +21,7 @@ public class Result /// /// Gets the error that caused the operation to fail. Throws if the operation was successful. - /// Use this after checking to ensure the operation was not + /// Use this after checking to ensure the operation was not /// successful. /// public TError Error => IsFailure ? _error! @@ -99,7 +99,7 @@ public sealed class Result : Result /// /// Gets the resulting value of the operation. Throws if the operation was not successful. - /// Use this after checking to ensure the operation was successful. + /// Use this after checking to ensure the operation was successful. /// public TResult Value => IsSuccess ? _value! : throw new InvalidOperationException("Cannot access the value of an unsuccessful result.", diff --git a/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs b/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs index 1532a65..eb58d6c 100644 --- a/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs +++ b/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs @@ -77,10 +77,10 @@ public void GetSizeTest() #endregion - #region IsInRange + #region Contains(UIntPtr) /// - /// Tests the method with specified addresses and ranges. + /// Tests the method with specified addresses and ranges. /// It should return the specified expected value. /// [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1000, ExpectedResult = true)] @@ -91,15 +91,15 @@ public void GetSizeTest() public bool IsInRangeTest(ulong start, ulong end, ulong address) { var range = new MemoryRange(new UIntPtr(start), new UIntPtr(end)); - return range.IsInRange(new UIntPtr(address)); + return range.Contains(new UIntPtr(address)); } #endregion - #region Contains + #region Contains(MemoryRange) /// - /// Tests the method with specified ranges. + /// Tests the method with specified ranges. /// It should return the specified expected value. /// [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1001, (ulong)0x1FFE, ExpectedResult = true)] @@ -119,6 +119,27 @@ public bool ContainsTest(ulong start, ulong end, ulong otherStart, ulong otherEn #endregion + #region DistanceTo + + /// + /// Tests the method with specified ranges and addresses. + /// It should return the specified expected value. + /// + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1000, ExpectedResult = (ulong)0)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x1FFF, ExpectedResult = (ulong)0)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x14A2, ExpectedResult = (ulong)0)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x0000, ExpectedResult = (ulong)0x1000)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x0FFF, ExpectedResult = (ulong)1)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x2000, ExpectedResult = (ulong)1)] + [TestCase((ulong)0x1000, (ulong)0x1FFF, (ulong)0x3000, ExpectedResult = (ulong)0x1001)] + public ulong DistanceToTest(ulong start, ulong end, ulong address) + { + var range = new MemoryRange(new UIntPtr(start), new UIntPtr(end)); + return range.DistanceTo(new UIntPtr(address)); + } + + #endregion + #region Overlaps /// diff --git a/test/MindControl.Test/AddressingTests/PointerExtensionsTest.cs b/test/MindControl.Test/AddressingTests/PointerExtensionsTest.cs new file mode 100644 index 0000000..90e03a8 --- /dev/null +++ b/test/MindControl.Test/AddressingTests/PointerExtensionsTest.cs @@ -0,0 +1,65 @@ +using NUnit.Framework; + +namespace MindControl.Test.AddressingTests; + +/// +/// Tests the class. +/// +public class PointerExtensionsTest +{ + /// + /// Tests the method. + /// + [TestCase((ulong)0x1000, (ulong)0x2000, ExpectedResult = (ulong)0x1000)] + [TestCase((ulong)0x2000, (ulong)0x1000, ExpectedResult = (ulong)0x1000)] + [TestCase((ulong)0x1000, (ulong)0x1000, ExpectedResult = (ulong)0)] + [TestCase((ulong)0, ulong.MaxValue, ExpectedResult = ulong.MaxValue)] + [TestCase(ulong.MaxValue, (ulong)0, ExpectedResult = ulong.MaxValue)] + public ulong DistanceToTest(ulong value1, ulong value2) + { + var ptr1 = new UIntPtr(value1); + var ptr2 = new UIntPtr(value2); + return ptr1.DistanceTo(ptr2); + } + + /// Test cases for . + public record GetRangeAroundTestCase(ulong Address, ulong Size, ulong ExpectedStart, ulong ExpectedEnd); + + private static GetRangeAroundTestCase[] _getRangeAroundTestCases = + { + new(1, 2, 0, 2), + new(0x1000, 0x2000, 0, 0x2000), + new(1, 4, 0, 3), + new(2, 3, 1, 3), + new(ulong.MaxValue, 8, ulong.MaxValue - 4, ulong.MaxValue) + }; + + /// + /// Tests the method. + /// + /// Test case to run. + [TestCaseSource(nameof(_getRangeAroundTestCases))] + public void GetRangeAroundTest(GetRangeAroundTestCase testCase) + { + var address = new UIntPtr(testCase.Address); + var range = address.GetRangeAround(testCase.Size); + Assert.That(range.Start.ToUInt64(), Is.EqualTo(testCase.ExpectedStart)); + Assert.That(range.End.ToUInt64(), Is.EqualTo(testCase.ExpectedEnd)); + } + + /// + /// Tests the method with a size of zero. + /// Expects an . + /// + [Test] + public void GetRangeAroundWithZeroSizeTest() + => Assert.Throws(() => ((UIntPtr)0x1000).GetRangeAround(0)); + + /// + /// Tests the method with a size of one. + /// Expects an . + /// + [Test] + public void GetRangeAroundWithOneSizeTest() + => Assert.Throws(() => ((UIntPtr)0x1000).GetRangeAround(1)); +} \ No newline at end of file diff --git a/test/MindControl.Test/AddressingTests/PointerPathTest.cs b/test/MindControl.Test/AddressingTests/PointerPathTest.cs index 383fd9f..cee263c 100644 --- a/test/MindControl.Test/AddressingTests/PointerPathTest.cs +++ b/test/MindControl.Test/AddressingTests/PointerPathTest.cs @@ -1,4 +1,3 @@ -using System.Numerics; using NUnit.Framework; namespace MindControl.Test.AddressingTests; @@ -399,7 +398,7 @@ private void AssertResultingAddress(PointerPath? result, ExpressionTestCase test Assert.That(result!.Expression, Is.EqualTo(testCase.Expression), testCase.Explanation); Assert.That(result.BaseModuleName, Is.EqualTo(testCase.ExpectedModuleName), testCase.Explanation); Assert.That(result.BaseModuleOffset, Is.EqualTo(testCase.ExpectedModuleOffset), testCase.Explanation); - Assert.That(result.PointerOffsets, Is.EquivalentTo(testCase.ExpectedPointerOffsets), testCase.Explanation); + Assert.That(result.PointerOffsets, Is.EquivalentTo(testCase.ExpectedPointerOffsets!), testCase.Explanation); Assert.That(result.IsStrictly64Bits, Is.EqualTo(testCase.Expect64BitOnly), testCase.Explanation); }); } diff --git a/test/MindControl.Test/MindControl.Test.csproj b/test/MindControl.Test/MindControl.Test.csproj index 10f4dd1..66e5404 100644 --- a/test/MindControl.Test/MindControl.Test.csproj +++ b/test/MindControl.Test/MindControl.Test.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 false enable enable @@ -9,15 +9,23 @@ - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + +/// Tests the features of the class related to code hooks. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryHookTest : ProcessMemoryTest +{ + /// + /// Tests the method. + /// The hook replaces a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the + /// long value in the target app, with a new MOV instruction that assigns a different value. + /// It is performed with full isolation, with the RAX register as an exception (because we want to interfere with + /// that register). + /// After hooking, we let the program run, and check that the output long value is the one written by the hook. + /// + [Test] + public void HookAndReplaceValueOfMovInstructionWithFullIsolationTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + + // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. + var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction, Register.RAX)); + + Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.Value.InjectedCodeReservation, Is.Not.Null); + Assert.That(hookResult.Value.Address, Is.EqualTo(movLongAddress)); + Assert.That(hookResult.Value.Length, Is.AtLeast(5)); + + ProceedToNextStep(); + ProceedToNextStep(); + + // After execution, the output must reflect the new value written by the hook. + Assert.That(FinalResults[5], Is.EqualTo("1234567890")); + } +} \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index 589d5e7..e251820 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -52,15 +52,210 @@ public void AllocateExecutableTest() Assert.That(allocation.IsExecutable, Is.True); Assert.That(TestProcessMemory!.Allocations, Has.Member(allocation)); } + + /// + /// Tests the method. + /// Performs two allocations: one with a range and one without a range. + /// The range starts after the expected range of the rangeless allocation. + /// Expect the rangeless allocation to be outside of the range, and the ranged allocation to be within the range. + /// + [Test] + public void AllocateWithinRangeTest() + { + var range = new MemoryRange(new UIntPtr(0x120000), UIntPtr.MaxValue); + var allocationWithoutRangeResult = TestProcessMemory!.Allocate(0x1000, false); + var allocationWithRangeResult = TestProcessMemory!.Allocate(0x1000, false, range); + Assert.That(allocationWithoutRangeResult.IsSuccess, Is.True); + Assert.That(allocationWithRangeResult.IsSuccess, Is.True); + Assert.That(range.Contains(allocationWithoutRangeResult.Value.Range), Is.False); + Assert.That(range.Contains(allocationWithRangeResult.Value.Range), Is.True); + } + + /// + /// Tests the method. + /// Performs two allocations: one with a specific "nearAddress", the other one with default parameters. + /// The one with a specific "nearAddress" should be allocated closer to the specified address than the other one. + /// + [Test] + public void AllocateNearAddressTest() + { + var nearAddress = new UIntPtr(0x400000000000); + var allocationWithNearAddressResult = TestProcessMemory!.Allocate(0x1000, false, nearAddress: nearAddress); + var allocationWithoutNearAddressResult = TestProcessMemory!.Allocate(0x1000, false); + Assert.That(allocationWithoutNearAddressResult.IsSuccess, Is.True); + Assert.That(allocationWithNearAddressResult.IsSuccess, Is.True); + + Assert.That(allocationWithNearAddressResult.Value.Range.DistanceTo(nearAddress), + Is.LessThan(allocationWithoutNearAddressResult.Value.Range.DistanceTo(nearAddress))); + } /// /// Tests the method with a zero size. - /// This should throw an exception. + /// This should return an . /// [Test] public void AllocateZeroTest() - => Assert.Throws(() => TestProcessMemory!.Allocate(0, false)); + { + var allocateResult = TestProcessMemory!.Allocate(0, false); + Assert.That(allocateResult.IsSuccess, Is.False); + Assert.That(allocateResult.Error, Is.InstanceOf()); + } + + /// + /// Tests the method. + /// Performs an allocation, and then calls the tested method with a size that fits the previously allocated range. + /// This should perform a reservation on the existing allocation. + /// + [Test] + public void ReserveWithAvailableAllocationTest() + { + var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; + var reservationResult = TestProcessMemory.Reserve(0x1000, false); + Assert.That(reservationResult.IsSuccess, Is.True); + var reservation = reservationResult.Value; + Assert.That(reservation.Range.GetSize(), Is.EqualTo(0x1000)); + Assert.That(reservation.ParentAllocation, Is.EqualTo(allocation)); + Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); + } + + /// + /// Tests the method. + /// Calls the tested method without any existing allocation. + /// This should perform a new allocation and a reservation on it. + /// + [Test] + public void ReserveWithoutAvailableAllocationTest() + { + var reservationResult = TestProcessMemory!.Reserve(0x1000, false); + Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); + Assert.That(reservationResult.Value.Range.GetSize(), Is.EqualTo(0x1000)); + } + + /// + /// Tests the method. + /// Performs a code allocation, and then calls the tested method with a size that fits the previously allocated + /// range, but for data, not code. + /// This should still perform a reservation on the existing allocation, because code allocations can be used for + /// data. + /// + [Test] + public void ReserveForDataWithAvailableCodeAllocationTest() + { + var allocation = TestProcessMemory!.Allocate(0x1000, true).Value; + var reservationResult = TestProcessMemory.Reserve(0x1000, false); + Assert.That(reservationResult.IsSuccess, Is.True); + var reservation = reservationResult.Value; + Assert.That(reservation.ParentAllocation, Is.EqualTo(allocation)); + Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); + } + + /// + /// Tests the method. + /// Performs a data allocation, and then calls the tested method with a size that fits the previously allocated + /// range, but for executable code, not data. + /// This should perform a new allocation with executable permissions (because data allocations cannot be used for + /// code), and a reservation on it. + /// + [Test] + public void ReserveForCodeWithAvailableDataAllocationTest() + { + var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; + var reservationResult = TestProcessMemory.Reserve(0x1000, true); + Assert.That(reservationResult.IsSuccess, Is.True); + var reservation = reservationResult.Value; + Assert.That(reservation.ParentAllocation, Is.Not.EqualTo(allocation)); + Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(2)); + Assert.That(reservation.ParentAllocation.IsExecutable, Is.True); + } + + /// + /// Tests the method. + /// Performs a code allocation, and then calls the tested method with a size that fits the previously allocated + /// range, and for code. + /// This should perform a reservation on the existing allocation. + /// + [Test] + public void ReserveForCodeWithAvailableCodeAllocationTest() + { + var allocation = TestProcessMemory!.Allocate(0x1000, true).Value; + var reservationResult = TestProcessMemory.Reserve(0x1000, true); + Assert.That(reservationResult.IsSuccess, Is.True); + var reservation = reservationResult.Value; + Assert.That(reservation.ParentAllocation, Is.EqualTo(allocation)); + Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); + } + + /// + /// Tests the method. + /// Performs an allocation of 0x1000 bytes, and then calls the tested method with a size of 0x2000 bytes. + /// This should perform a new allocation and a reservation on it. + /// + [Test] + public void ReserveTooLargeForAvailableAllocationsTest() + { + var allocation = TestProcessMemory!.Allocate(0x1000, true).Value; + var reservationResult = TestProcessMemory.Reserve(0x2000, true); + Assert.That(reservationResult.IsSuccess, Is.True); + var reservation = reservationResult.Value; + Assert.That(reservation.ParentAllocation, Is.Not.EqualTo(allocation)); + Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(2)); + } + + /// + /// Tests the method. + /// Performs an allocation, and then calls the tested method with a size that fits the previously allocated range, + /// but with a limit range that is outside of the existing allocated range. + /// This should perform a new allocation and a reservation on it. + /// + [Test] + public void ReserveWithLimitRangeTest() + { + var range = new MemoryRange(unchecked((UIntPtr)0x400000000000), UIntPtr.MaxValue); + var allocation = TestProcessMemory!.Allocate(0x1000, true).Value; + var reservationResult = TestProcessMemory.Reserve(0x1000, true, range); + Assert.That(reservationResult.IsSuccess, Is.True); + var reservation = reservationResult.Value; + Assert.That(reservation.ParentAllocation, Is.Not.EqualTo(allocation)); + Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(2)); + Assert.That(range.Contains(reservation.Address)); + } + + /// + /// Tests the method. + /// Performs 3 allocations at various ranges, and then calls the tested method with a size that fits all previously + /// allocated ranges, but with a near address that is closer to the second allocation. + /// This should perform a reservation on the second allocation. + /// + [Test] + public void ReserveWithNearAddressTest() + { + TestProcessMemory!.Allocate(0x1000, true, + new MemoryRange(unchecked((UIntPtr)0x400000000000), UIntPtr.MaxValue)); + var allocation2 = TestProcessMemory!.Allocate(0x1000, true, + new MemoryRange(unchecked((UIntPtr)0x200000000000), UIntPtr.MaxValue)).Value; + TestProcessMemory!.Allocate(0x1000, true, + new MemoryRange(unchecked((UIntPtr)0x4B0000000000), UIntPtr.MaxValue)); + + var reservationResult = TestProcessMemory.Reserve(0x1000, true, nearAddress:unchecked((UIntPtr)0x2000051C0000)); + Assert.That(reservationResult.IsSuccess, Is.True); + var reservation = reservationResult.Value; + Assert.That(reservation.ParentAllocation, Is.EqualTo(allocation2)); + Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(3)); + } + /// + /// Tests the method with a size of zero. + /// Expects the result to be an . + /// + [Test] + public void ReserveZeroTest() + { + var reserveResult = TestProcessMemory!.Reserve(0, false); + Assert.That(reserveResult.IsSuccess, Is.False); + Assert.That(reserveResult.Error, Is.InstanceOf()); + } + /// /// Tests the method. /// Stores a byte array in an allocated range and verifies that the value has been stored properly. diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs index c206492..4a78228 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs @@ -21,7 +21,7 @@ public void EvaluateOnKnownPointerTest() var result = TestProcessMemory!.EvaluateMemoryAddress($"{OuterClassPointer:X}+10,10,0"); Assert.That(result.IsSuccess, Is.True); - Assert.That(result.Value, Is.EqualTo((UIntPtr)ulong.MaxValue)); + Assert.That(result.Value, Is.EqualTo(unchecked((UIntPtr)ulong.MaxValue))); } /// @@ -56,7 +56,7 @@ public void EvaluateWithUnreadableAddressTest() var error = result.Error; Assert.That(error, Is.TypeOf()); var pathError = (PathEvaluationFailureOnPointerReadFailure)error; - Assert.That(pathError.Address, Is.EqualTo((UIntPtr)ulong.MaxValue)); + Assert.That(pathError.Address, Is.EqualTo(unchecked((UIntPtr)ulong.MaxValue))); Assert.That(pathError.Details, Is.TypeOf()); var readFailure = (ReadFailureOnSystemRead)pathError.Details; Assert.That(readFailure.Details, Is.TypeOf()); diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs index b1ea6ec..cf5fa77 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs @@ -28,7 +28,7 @@ public void FindBytesWithKnownFixedBytesPatternTest() Assert.That(results, Has.Length.EqualTo(1)); // Verify that the result is within the range of the main module - Assert.That(range!.Value.IsInRange(results.Single()), Is.True); + Assert.That(range!.Value.Contains(results.Single()), Is.True); } /// @@ -55,7 +55,7 @@ public void FindBytesWithKnownMaskedBytesPatternTest() // Verify that the results are within the range of the main module foreach (var result in results) - Assert.That(range!.Value.IsInRange(result), Is.True); + Assert.That(range!.Value.Contains(result), Is.True); } /// @@ -78,7 +78,7 @@ public void FindBytesWithKnownPartialMasksBytesPatternTest() Assert.That(results, Has.Length.EqualTo(1)); // Verify that the result is within the range of the main module - Assert.That(range!.Value.IsInRange(results.Single()), Is.True); + Assert.That(range!.Value.Contains(results.Single()), Is.True); } /// @@ -102,6 +102,6 @@ public async Task FindBytesAsyncWithKnownFixedBytesPatternTest() Assert.That(results, Has.Length.EqualTo(1)); // Verify that the result is within the range of the main module - Assert.That(range!.Value.IsInRange(results.Single()), Is.True); + Assert.That(range!.Value.Contains(results.Single()), Is.True); } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index 20ba828..9475439 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -261,7 +261,7 @@ public void ReadBytesPartialOnPartiallyUnreadableRangeTest() public void ReadTwoStepGenericTest(Type targetType, int pointerOffset, object? expectedResultBeforeBreak, object? expectedResultAfterBreak) { - UIntPtr targetIntAddress = OuterClassPointer + pointerOffset; + UIntPtr targetIntAddress = OuterClassPointer + (UIntPtr)pointerOffset; object resultBefore = TestProcessMemory!.Read(targetType, targetIntAddress).Value; Assert.That(resultBefore, Is.EqualTo(expectedResultBeforeBreak)); ProceedToNextStep(); From a51298ff7a80f912363c1ee6b9a4bbf8aba64a98 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 15 Jun 2024 10:47:57 +0200 Subject: [PATCH 22/66] Code extensions unit tests --- .../Code/ProcessMemory.Code.cs | 10 +- .../Results/CodeWritingFailure.cs | 2 +- .../ProcessMemoryCodeExtensionsTest.cs | 147 ++++++++++++++++++ .../ProcessMemoryHookExtensionsTest.cs | 95 +++++++++++ .../CodeExtensions/ProcessMemoryHookTest.cs | 44 ------ .../ProcessMemoryTests/ProcessMemoryTest.cs | 46 ++++++ .../ProcessMemoryWriteTest.cs | 41 ----- 7 files changed, 295 insertions(+), 90 deletions(-) create mode 100644 test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs create mode 100644 test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs delete mode 100644 test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookTest.cs diff --git a/src/MindControl.Code/Code/ProcessMemory.Code.cs b/src/MindControl.Code/Code/ProcessMemory.Code.cs index 3d7c6ba..63b2cf3 100644 --- a/src/MindControl.Code/Code/ProcessMemory.Code.cs +++ b/src/MindControl.Code/Code/ProcessMemory.Code.cs @@ -20,7 +20,7 @@ public static class ProcessMemoryCodeExtensions /// Number of consecutive instructions to replace. Default is 1. /// A result holding either a code change instance, allowing you to revert modifications, or a code writing /// failure. - public static Result RemoveCodeAt(this ProcessMemory processMemory, + public static Result DisableCodeAt(this ProcessMemory processMemory, PointerPath pointerPath, int instructionCount = 1) { if (instructionCount < 1) @@ -31,7 +31,7 @@ public static Result RemoveCodeAt(this ProcessMe if (addressResult.IsFailure) return new CodeWritingFailureOnPathEvaluation(addressResult.Error); - return processMemory.RemoveCodeAt(addressResult.Value, instructionCount); + return processMemory.DisableCodeAt(addressResult.Value, instructionCount); } /// @@ -43,9 +43,11 @@ public static Result RemoveCodeAt(this ProcessMe /// Number of consecutive instructions to replace. Default is 1. /// A result holding either a code change instance, allowing you to revert modifications, or a code writing /// failure. - public static Result RemoveCodeAt(this ProcessMemory processMemory, + public static Result DisableCodeAt(this ProcessMemory processMemory, UIntPtr address, int instructionCount = 1) { + if (address == UIntPtr.Zero) + return new CodeWritingFailureOnZeroPointer(); if (instructionCount < 1) return new CodeWritingFailureOnInvalidArguments( "The number of instructions to replace must be at least 1."); @@ -63,7 +65,7 @@ public static Result RemoveCodeAt(this ProcessMe { var instruction = decoder.Decode(); if (instruction.IsInvalid) - return new CodeWritingFailureOnDecodingFailure(decoder.LastError); + return new CodeWritingFailureOnDecoding(decoder.LastError); fullLength += (ulong)instruction.Length; } diff --git a/src/MindControl.Code/Results/CodeWritingFailure.cs b/src/MindControl.Code/Results/CodeWritingFailure.cs index d9b55f4..e4da14d 100644 --- a/src/MindControl.Code/Results/CodeWritingFailure.cs +++ b/src/MindControl.Code/Results/CodeWritingFailure.cs @@ -95,7 +95,7 @@ public record CodeWritingFailureOnReadFailure(ReadFailure Details) /// Represents a failure that occurred while writing code to a target process when a disassembling operation failed. /// /// Error code that describes the failure. -public record CodeWritingFailureOnDecodingFailure(DecoderError Error) +public record CodeWritingFailureOnDecoding(DecoderError Error) : CodeWritingFailure(CodeWritingFailureReason.DecodingFailure) { /// Returns a string that represents the current object. diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs new file mode 100644 index 0000000..29a99c8 --- /dev/null +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs @@ -0,0 +1,147 @@ +using MindControl.Code; +using MindControl.Results; +using NUnit.Framework; + +namespace MindControl.Test.ProcessMemoryTests.CodeExtensions; + +/// +/// Tests the features of the class related to code manipulation. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryCodeExtensionsTest : ProcessMemoryTest +{ + /// + /// Tests the method. + /// The method is called on a MOV instruction that changes the value of the long value in the target app after the + /// first step. + /// After disabling the instruction, we let the program run to the end, and check that the output long value is the + /// original one (it was not modified because we disabled the instruction). + /// + [Test] + public void DisableCodeAtTest() + { + var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First() + 10; + var result = TestProcessMemory.DisableCodeAt(movLongAddress); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value.Address, Is.EqualTo(movLongAddress)); + Assert.That(result.Value.Length, Is.AtLeast(1)); // We don't care how long it is but we check that it is set. + + ProceedUntilProcessEnds(); + + // Test that the output long at index 5 is the first value set when the program starts, not the one that was + // supposed to be set by the disabled instruction. + AssertFinalResults(5, "-65746876815103"); + } + + /// + /// Tests the method. + /// This is a variant of that uses a pointer path instead of an address. + /// + [Test] + public void DisableCodeAtWithPointerPathTest() + { + var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First() + 10; + var pointerPath = movLongAddress.ToString("X"); + var result = TestProcessMemory.DisableCodeAt(pointerPath); + Assert.That(result.IsSuccess, Is.True); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "-65746876815103"); + } + + /// + /// Tests the method. + /// The method is called on a series of MOV instruction that changes the value of the long and ulong values in the + /// target app after the first step. + /// After disabling the instructions, we let the program run to the end, and check that the output long and ulong + /// values are the original ones (they were not modified because we disabled the instructions). + /// + [Test] + public void DisableCodeAtWithMultipleInstructionsTest() + { + var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + var result = TestProcessMemory.DisableCodeAt(movLongAddress, 5); + Assert.That(result.IsSuccess, Is.True); + + ProceedUntilProcessEnds(); + + // Test that the output long at index 5 and ulong at index 6 are the first values set when the program starts, + // not the ones that were supposed to be set by the disabled instructions. + Assert.That(FinalResults[5], Is.EqualTo("-65746876815103")); + Assert.That(FinalResults[6], Is.EqualTo("76354111324644")); + } + + /// + /// Tests the method. + /// The method is called on a MOV instruction that changes the value of the long value in the target app after the + /// first step. + /// After disabling the instruction, we revert the change. + /// We then let the program run to the end, and check that the output long value is the expected one (the one set by + /// the instruction we disabled and reverted). + /// + [Test] + public void DisableCodeAtRevertTest() + { + var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First() + 10; + var result = TestProcessMemory.DisableCodeAt(movLongAddress); + result.Value.Revert(); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, ExpectedFinalValues[5]); + } + + /// + /// Tests the method. + /// The method is called with a zero pointer. + /// Expects a . + /// + [Test] + public void DisableCodeAtWithZeroAddressTest() + { + var result = TestProcessMemory!.DisableCodeAt(UIntPtr.Zero); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + } + + /// + /// Tests the method. + /// The method is called with an instruction count of zero. + /// Expects a . + /// + [Test] + public void DisableCodeAtWithInvalidInstructionCountTest() + { + var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First() + 10; + var result = TestProcessMemory.DisableCodeAt(movLongAddress, 0); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + } + + /// + /// Tests the method. + /// The method is called with a pointer path that does not point to a valid address. + /// Expects a . + /// + [Test] + public void DisableCodeAtWithBadPointerPathTest() + { + var result = TestProcessMemory!.DisableCodeAt("bad pointer path"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + } + + /// + /// Tests the method. + /// The method is called on a freshly allocated memory address that holds only FF bytes instead of valid code. + /// Expects a . + /// + [Test] + public void DisableCodeAtWithBadInstructionsTest() + { + var address = TestProcessMemory!.Reserve(0x1000, true).Value.Address; + TestProcessMemory.WriteBytes(address, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }); + var result = TestProcessMemory.DisableCodeAt(address); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + } +} \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs new file mode 100644 index 0000000..4f60299 --- /dev/null +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs @@ -0,0 +1,95 @@ +using Iced.Intel; +using MindControl.Hooks; +using NUnit.Framework; + +namespace MindControl.Test.ProcessMemoryTests.CodeExtensions; + +/// +/// Tests the features of the class related to code hooks. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryHookExtensionsTest : ProcessMemoryTest +{ + /// + /// Tests the method. + /// The hook replaces a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the + /// long value in the target app, with a new MOV instruction that assigns a different value. + /// It is performed with full isolation, with the RAX register as an exception (because we want to interfere with + /// that register). + /// After hooking, we let the program run, and check that the output long value is the one written by the hook. + /// + [Test] + public void HookAndReplaceValueOfMovInstructionWithIsolationTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + + // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. + var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction, Register.RAX)); + + Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.Value.InjectedCodeReservation, Is.Not.Null); + Assert.That(hookResult.Value.Address, Is.EqualTo(movLongAddress)); + Assert.That(hookResult.Value.Length, Is.AtLeast(5)); + + ProceedUntilProcessEnds(); + + // After execution, the long in the output at index 5 must reflect the new value written by the hook. + AssertFinalResults(5, "1234567890"); + } + + /// + /// Tests the method. + /// The hook targets a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the + /// long value in the target app, and inserts a new MOV instruction that assigns a different value after the + /// instruction. + /// It is performed with full isolation, without exceptions. + /// After hooking, we let the program run, and check the output. The long value must be the expected, original one, + /// because the options specified that the registers should be isolated from the injected code. + /// + [Test] + public void HookAndInsertValueOfMovInstructionWithCompleteIsolationTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + + // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. + var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, + new HookOptions(HookExecutionMode.ExecuteOriginalInstructionFirst)); + Assert.That(hookResult.IsSuccess, Is.True); + + ProceedUntilProcessEnds(); + + // After execution, the long in the output at index 5 must reflect the new value written by the hook. + AssertExpectedFinalResults(); + } + + /// + /// Tests the method. + /// The hook replaces a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the + /// long value in the target app, with a new MOV instruction that assigns a different value. + /// After hooking, we revert the hook. + /// We then let the program run, and check that the output long value is the original, expected one. + /// + [Test] + public void HookRevertTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + + // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. + var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction, Register.RAX)); + hookResult.Value.Revert(); // Revert the hook to restore the original code. + + ProceedUntilProcessEnds(); + AssertFinalResults(5, ExpectedFinalValues[5]); + } +} \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookTest.cs deleted file mode 100644 index 8cf1a9b..0000000 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookTest.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Iced.Intel; -using MindControl.Hooks; -using NUnit.Framework; - -namespace MindControl.Test.ProcessMemoryTests.CodeExtensions; - -/// -/// Tests the features of the class related to code hooks. -/// -[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryHookTest : ProcessMemoryTest -{ - /// - /// Tests the method. - /// The hook replaces a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the - /// long value in the target app, with a new MOV instruction that assigns a different value. - /// It is performed with full isolation, with the RAX register as an exception (because we want to interfere with - /// that register). - /// After hooking, we let the program run, and check that the output long value is the one written by the hook. - /// - [Test] - public void HookAndReplaceValueOfMovInstructionWithFullIsolationTest() - { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); - - // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. - var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, - new HookOptions(HookExecutionMode.ReplaceOriginalInstruction, Register.RAX)); - - Assert.That(hookResult.IsSuccess, Is.True); - Assert.That(hookResult.Value.InjectedCodeReservation, Is.Not.Null); - Assert.That(hookResult.Value.Address, Is.EqualTo(movLongAddress)); - Assert.That(hookResult.Value.Length, Is.AtLeast(5)); - - ProceedToNextStep(); - ProceedToNextStep(); - - // After execution, the output must reflect the new value written by the hook. - Assert.That(FinalResults[5], Is.EqualTo("1234567890")); - } -} \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs index 230f0f1..6b1d18c 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs @@ -109,4 +109,50 @@ protected void ProceedUntilProcessEnds() ProceedToNextStep(); ProceedToNextStep(); } + + /// + /// Stores the line-by-line expected output of the target app. + /// + protected static readonly string[] ExpectedFinalValues = + [ + "False", + "220", + "987411", + "444763", + "ThisIsALongerStrîngWith文字化けチェック", + "-777654646516513", + "34411111111164", + "173", + "64646321", + "7777777777777", + "-8888", + "9999", + "-123444.15", + "-99879416311.4478", + "85,102,119,136" + ]; + + /// + /// Asserts that among the final results output by the target app, the one at the given index matches the + /// expected value, and all the other results are the known, untouched values. + /// + /// Index of the final result to check against the . + /// Expected value of the final result at the specified index. + protected void AssertFinalResults(int index, string expectedValue) + { + for (int i = 0; i < ExpectedFinalValues.Length; i++) + { + string expectedValueAtIndex = i == index ? expectedValue : ExpectedFinalValues[i]; + Assert.That(FinalResults.ElementAtOrDefault(i), Is.EqualTo(expectedValueAtIndex)); + } + } + + /// + /// Asserts that the final results output by the target app match the expected values. + /// + protected void AssertExpectedFinalResults() + { + for (int i = 0; i < ExpectedFinalValues.Length; i++) + Assert.That(FinalResults.ElementAtOrDefault(i), Is.EqualTo(ExpectedFinalValues[i])); + } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs index b30dbb0..b73c7a4 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs @@ -8,47 +8,6 @@ namespace MindControl.Test.ProcessMemoryTests; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ProcessMemoryWriteTest : ProcessMemoryTest { - #region Common - - /// - /// Stores the line-by-line expected output of the target app. - /// - private static readonly string[] ExpectedFinalValues = - { - "False", - "220", - "987411", - "444763", - "ThisIsALongerStrîngWith文字化けチェック", - "-777654646516513", - "34411111111164", - "173", - "64646321", - "7777777777777", - "-8888", - "9999", - "-123444.15", - "-99879416311.4478", - "85,102,119,136" - }; - - /// - /// Asserts that among the final results output by the target app, the one at the given index matches the - /// expected value, and all the other results are the known, untouched values. - /// - /// Index of the final result to check against the . - /// Expected value of the final result at the specified index. - private void AssertFinalResults(int index, string expectedValue) - { - for (int i = 0; i < ExpectedFinalValues.Length; i++) - { - string expectedValueAtIndex = i == index ? expectedValue : ExpectedFinalValues[i]; - Assert.That(FinalResults.ElementAtOrDefault(i), Is.EqualTo(expectedValueAtIndex)); - } - } - - #endregion - /// /// Write a value in the target app memory, and assert that the output of the app for that specific value (and no /// other value) reflects the change. From 46a5c367c3fdf178d50f5b87dae76cdd3c3e5817 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 15 Jun 2024 15:48:29 +0200 Subject: [PATCH 23/66] More reasonable hook options --- .../Code/ProcessMemory.Code.cs | 2 +- .../Extensions/AssemblerExtensions.cs | 46 ++-- src/MindControl.Code/Hooks/HookOptions.cs | 211 ++------------- src/MindControl.Code/Hooks/HookRegister.cs | 240 ++++++++++++++++++ .../Hooks/ProcessMemory.Hooks.cs | 56 ++-- src/MindControl/Addressing/MemoryRange.cs | 9 +- src/MindControl/Addressing/PointerOffset.cs | 4 +- src/MindControl/Addressing/PointerPath.cs | 20 +- .../Allocation/MemoryAllocation.cs | 3 +- .../Internal/ConversionExtensions.cs | 6 +- .../Native/IOperatingSystemService.cs | 13 +- src/MindControl/Native/Win32Service.cs | 40 +-- .../ProcessMemory/ProcessMemory.Addressing.cs | 2 +- .../ProcessMemory/ProcessMemory.Allocation.cs | 6 +- .../ProcessMemory/ProcessMemory.FindBytes.cs | 2 +- .../ProcessMemory/ProcessMemory.Write.cs | 6 +- .../ProcessMemory/ProcessMemory.cs | 10 +- .../Results/PathEvaluationFailure.cs | 6 +- src/MindControl/Results/ReadFailure.cs | 6 +- src/MindControl/Results/StringReadFailure.cs | 4 +- src/MindControl/Results/WriteFailure.cs | 6 +- .../AddressingTests/PointerPathTest.cs | 22 +- .../ProcessMemoryHookExtensionsTest.cs | 23 +- 23 files changed, 415 insertions(+), 328 deletions(-) create mode 100644 src/MindControl.Code/Hooks/HookRegister.cs diff --git a/src/MindControl.Code/Code/ProcessMemory.Code.cs b/src/MindControl.Code/Code/ProcessMemory.Code.cs index 63b2cf3..d25aa28 100644 --- a/src/MindControl.Code/Code/ProcessMemory.Code.cs +++ b/src/MindControl.Code/Code/ProcessMemory.Code.cs @@ -58,7 +58,7 @@ public static Result DisableCodeAt(this ProcessM using var stream = processMemory.GetMemoryStream(address); var codeReader = new StreamCodeReader(stream); - var decoder = Decoder.Create(processMemory.Is64Bits ? 64 : 32, codeReader); + var decoder = Decoder.Create(processMemory.Is64Bit ? 64 : 32, codeReader); ulong fullLength = 0; for (int i = 0; i < instructionCount; i++) diff --git a/src/MindControl.Code/Extensions/AssemblerExtensions.cs b/src/MindControl.Code/Extensions/AssemblerExtensions.cs index 7b782f3..fe8c7e5 100644 --- a/src/MindControl.Code/Extensions/AssemblerExtensions.cs +++ b/src/MindControl.Code/Extensions/AssemblerExtensions.cs @@ -12,14 +12,14 @@ public static class AssemblerExtensions /// Checks if the register is compatible with the target architecture and can be saved and restored individually. /// /// Register to check. - /// True if the target architecture is 64 bits, false if it is 32 bits. + /// True if the target architecture is 64-bit, false if it is 32-bit. /// True if the register is compatible with the target architecture, false otherwise. /// For ST registers, this method will always return false, because ST registers must be saved and restored /// as a whole (to preserve the whole FPU stack). - internal static bool IsIndividualPreservationSupported(this Register register, bool is64Bits) + internal static bool IsIndividualPreservationSupported(this Register register, bool is64Bit) { - return (is64Bits && register.IsGPR64()) || (!is64Bits && register.IsGPR32()) - || (register.IsXMM() && register.GetNumber() <= GetMaxXmmRegisterNumber(is64Bits)) + return (is64Bit && register.IsGPR64()) || (!is64Bit && register.IsGPR32()) + || (register.IsXMM() && register.GetNumber() <= GetMaxXmmRegisterNumber(is64Bit)) || register.IsST() || register.IsMM(); } @@ -27,9 +27,9 @@ internal static bool IsIndividualPreservationSupported(this Register register, b /// /// Gets the maximum number of XMM registers available for the target architecture. /// - /// True if the target architecture is 64 bits, false if it is 32 bits. + /// True if the target architecture is 64-bit, false if it is 32-bit. /// The maximum number of XMM registers available for the target architecture. - private static int GetMaxXmmRegisterNumber(bool is64Bits) => is64Bits ? 15 : 7; + private static int GetMaxXmmRegisterNumber(bool is64Bit) => is64Bit ? 15 : 7; /// /// Saves the given register to the stack (push it on the stack using instructions that depend on the register and @@ -91,10 +91,10 @@ public static void SaveRegister(this Assembler assembler, Register register) /// Gets the size in bytes of the instructions needed to save the given register to the stack. /// /// Register to save. - /// True if the target architecture is 64 bits, false if it is 32 bits. + /// True if the target architecture is 64-bit, false if it is 32-bit. /// The size in bytes of the instructions needed to save the register to the stack. /// Thrown if the register is not supported. - internal static int GetSizeOfSaveInstructions(Register register, bool is64Bits) + internal static int GetSizeOfSaveInstructions(Register register, bool is64Bit) { // Some registers are pushed on 2-byte instructions if (TwoBytePushGprRegisters.Contains(register)) @@ -113,8 +113,8 @@ internal static int GetSizeOfSaveInstructions(Register register, bool is64Bits) // Additionally, the movq instruction for XMM registers with numbers 8 and above (only available in x64) is // one byte longer. - int subSize = is64Bits ? 4 : 3; - int movSize = is64Bits ? (register.GetNumber() >= 8 ? 6 : 5) : 4; + int subSize = is64Bit ? 4 : 3; + int movSize = is64Bit ? (register.GetNumber() >= 8 ? 6 : 5) : 4; return subSize + movSize; } @@ -178,11 +178,11 @@ public static void RestoreRegister(this Assembler assembler, Register register) /// Gets the size in bytes of the instructions needed to restore the given register to the stack. /// /// Register to save. - /// True if the target architecture is 64 bits, false if it is 32 bits. + /// True if the target architecture is 64-bit, false if it is 32-bit. /// The size in bytes of the instructions needed to restore the register from the stack. /// Thrown if the register is not supported. - internal static int GetSizeOfRestoreInstructions(Register register, bool is64Bits) - => GetSizeOfSaveInstructions(register, is64Bits); // The instructions are the opposite, but the size is the same + internal static int GetSizeOfRestoreInstructions(Register register, bool is64Bit) + => GetSizeOfSaveInstructions(register, is64Bit); // The instructions are the opposite, but the size is the same /// /// Saves the FPU stack state as a whole (pushes all 8 ST registers to the stack). @@ -216,9 +216,9 @@ public static void SaveFpuStack(this Assembler assembler) /// /// Gets the size in bytes of the instructions needed to save the FPU stack state as a whole. /// - /// True if the target architecture is 64 bits, false if it is 32 bits. + /// True if the target architecture is 64-bit, false if it is 32-bit. /// The size in bytes of the instructions needed to save the FPU stack state as a whole. - internal static int GetSizeOfFpuStackSave(bool is64Bits) + internal static int GetSizeOfFpuStackSave(bool is64Bit) { // Example in x86: // sub esp, 64 ; 83 EC 40 (3 bytes) @@ -230,7 +230,7 @@ internal static int GetSizeOfFpuStackSave(bool is64Bits) // For x64, there is an added REX prefix byte (0x48) for each instruction. // So it should be 3 + 3 + 4 * 7 = 6 + 28 = 34 bytes for x86 and 4 + 4 + 5 * 7 = 8 + 35 = 43 bytes for x64. - return is64Bits ? 43 : 34; + return is64Bit ? 43 : 34; } /// @@ -260,10 +260,10 @@ public static void RestoreFpuStack(this Assembler assembler) /// /// Gets the size in bytes of the instructions needed to restore the FPU stack state as a whole. /// - /// True if the target architecture is 64 bits, false if it is 32 bits. + /// True if the target architecture is 64-bit, false if it is 32-bit. /// The size in bytes of the instructions needed to restore the FPU stack state as a whole. - internal static int GetSizeOfFpuStackRestore(bool is64Bits) - => GetSizeOfFpuStackSave(is64Bits); // The instructions are the opposite, but the size is the same + internal static int GetSizeOfFpuStackRestore(bool is64Bit) + => GetSizeOfFpuStackSave(is64Bit); // The instructions are the opposite, but the size is the same /// /// Saves the CPU flags to the stack (pushes the flags to the stack). @@ -280,9 +280,9 @@ internal static void SaveFlags(this Assembler assembler) /// /// Gets the size in bytes of the instructions needed to save the CPU flags. /// - /// True if the target architecture is 64 bits, false if it is 32 bits. + /// True if the target architecture is 64-bit, false if it is 32-bit. /// The size in bytes of the instructions needed to save the CPU flags. - internal static int GetSizeOfFlagsSave(bool is64Bits) => 1; // pushf is always single-byte + internal static int GetSizeOfFlagsSave(bool is64Bit) => 1; // pushf is always single-byte /// /// Restores the CPU flags from the stack (pops the flags from the stack). @@ -299,9 +299,9 @@ internal static void RestoreFlags(this Assembler assembler) /// /// Gets the size in bytes of the instructions needed to restore the CPU flags. /// - /// True if the target architecture is 64 bits, false if it is 32 bits. + /// True if the target architecture is 64-bit, false if it is 32-bit. /// The size in bytes of the instructions needed to restore the CPU flags. - internal static int GetSizeOfFlagsRestore(bool is64Bits) => 1; // popf is always single-byte + internal static int GetSizeOfFlagsRestore(bool is64Bit) => 1; // popf is always single-byte /// /// Attempts to assemble the instructions registered on this assembler instance and return the resulting bytes. diff --git a/src/MindControl.Code/Hooks/HookOptions.cs b/src/MindControl.Code/Hooks/HookOptions.cs index 8b614b8..0f7cfe3 100644 --- a/src/MindControl.Code/Hooks/HookOptions.cs +++ b/src/MindControl.Code/Hooks/HookOptions.cs @@ -25,70 +25,6 @@ public enum HookExecutionMode ReplaceOriginalInstruction } -/// -/// Flags that define how the injected code is isolated from the original code. -/// -[Flags] -public enum HookIsolationMode -{ - /// - /// No isolation. Modifications to flags and registers in the injected code will affect the original code, unless - /// the injected code contains instructions to save and restore the state in itself. - /// - None = 0, - - /// - /// Save and restore CPU flags. This allows the injected code to modify flags without affecting the original code. - /// This option is a flag, it can be combined with other options. - /// - PreserveFlags = 1, - - /// - /// Save and restore general-purpose registers. This allows the injected code to modify most registers without - /// affecting the original code. - /// This option is a flag, it can be combined with other options. - /// - PreserveGeneralPurposeRegisters = 2, - - /// - /// Save and restore XMM registers. This allows the injected code to modify XMM registers (for floating-point - /// operations) without affecting the original code. - /// This option is a flag, it can be combined with other options. - /// - PreserveXmmRegisters = 4, - - /// - /// Save and restore the FPU stack. This allows the injected code to modify the FPU stack without affecting the - /// original code. Note that the FPU stack is not used in modern code, so this option is usually not needed. - /// This option is a flag, it can be combined with other options. - /// - PreserveFpuStack = 8, - - /// - /// Save and restore the FPU stack. This allows the injected code to modify the FPU stack without affecting the - /// original code. Note that the FPU stack is not used in modern code, so this option is usually not needed. - /// This option is a flag, it can be combined with other options. - /// - PreserveMmRegisters = 16, - - /// - /// This option is a combination of the , , - /// and options. - /// This allows the injected code to modify flags and registers without affecting the original code. It is the - /// recommended option. - /// - FullIsolation = PreserveFlags | PreserveGeneralPurposeRegisters | PreserveXmmRegisters, - - /// - /// This option is a combination of all isolation options. - /// It allows the injected code to modify flags and registers without affecting the original code. It also includes - /// the FPU stack and MM registers, which are usually not needed. This will make the hook code slower. - /// Prefer unless you know ST and MM registers are used both in the original code and - /// the injected code. - /// - FullCompatibilityIsolation = FullIsolation | PreserveFpuStack | PreserveMmRegisters -} - /// /// Defines how the hook jump should be performed. /// @@ -113,65 +49,11 @@ public enum HookJumpMode /// public class HookOptions { - /// - /// Lists the general purpose registers that are preserved by the - /// isolation flag. - /// - private static readonly Register[] PreservableGeneralPurposeRegisters = - [ - // General purpose 32-bit registers - Register.EAX, Register.EBX, Register.ECX, Register.EDX, - Register.ESI, Register.EDI, Register.EBP, Register.ESP, - - // General purpose 64-bit registers - Register.RAX, Register.RBX, Register.RCX, Register.RDX, - Register.RSI, Register.RDI, Register.RBP, Register.RSP, - Register.R8, Register.R9, Register.R10, Register.R11, - Register.R12, Register.R13, Register.R14, Register.R15 - ]; - - /// - /// Lists the XMM registers to be preserved by the isolation - /// flag. - /// - private static readonly Register[] XmmRegisters = - [ - // (8-15 are 64-bit only) - Register.XMM0, Register.XMM1, Register.XMM2, Register.XMM3, - Register.XMM4, Register.XMM5, Register.XMM6, Register.XMM7, - Register.XMM8, Register.XMM9, Register.XMM10, Register.XMM11, - Register.XMM12, Register.XMM13, Register.XMM14, Register.XMM15 - ]; - - /// - /// Lists the MM registers to be preserved by the isolation - /// flag. - /// - private static readonly Register[] MmRegisters = - [ - Register.MM0, Register.MM1, Register.MM2, Register.MM3, - Register.MM4, Register.MM5, Register.MM6, Register.MM7 - ]; - /// /// Gets the execution mode of the injected code in relation to the original code. Defines whether the original /// code should be overwritten, executed before, or executed after the injected code. /// public HookExecutionMode ExecutionMode { get; init; } - - /// - /// Gets the isolation mode flags defining how the injected code is isolated from the original code. - /// Depending on the mode, instructions may be prepended and appended to the injected code to save and restore - /// registers and flags, allowing the injected code to run without affecting the original code. - /// - public HookIsolationMode IsolationMode { get; init; } - - /// - /// Gets the registers to exclude from preservation, regardless of the . - /// Use this if you want specific registers to NOT be saved and restored, either to improve performance or to - /// modify the behavior of the original code. - /// - public Register[] RegistersExcludedFromPreservation { get; init; } /// /// Gets the jump mode, which defines what kind of jump should be used to redirect the code flow to the injected @@ -181,104 +63,63 @@ public class HookOptions public HookJumpMode JumpMode { get; init; } /// - /// Initializes a new instance of the class with the given values. - /// The is set to . + /// Gets a collection of registers that should be saved before the injected code is executed, and restored after it + /// is executed. This is used to isolate the injected code from the original code, to prevent it from affecting the + /// original code's behavior or causing crashes. /// - /// Execution mode of the injected code in relation to the original code. Defines - /// whether the original code should be overwritten, executed before, or executed after the injected code. - /// Optional registers to exclude from preservation, regardless of - /// the . Use this if you want specific registers to NOT be saved and restored, either to - /// improve performance or to modify the behavior of the original code. - public HookOptions(HookExecutionMode executionMode, params Register[] registersExcludedFromPreservation) - : this(executionMode, HookIsolationMode.FullIsolation, HookJumpMode.NearJumpWithFallbackOnFarJump, - registersExcludedFromPreservation) { } + public HookRegister[] RegistersToPreserve { get; init; } /// /// Initializes a new instance of the class with the given values. /// /// Execution mode of the injected code in relation to the original code. Defines /// whether the original code should be overwritten, executed before, or executed after the injected code. - /// Isolation mode flags defining how the injected code is isolated from the original - /// code. Depending on the mode, instructions may be prepended and appended to the injected code to save and - /// restore registers and flags, allowing the injected code to run without affecting the original code. - /// Optional registers to exclude from preservation, regardless of - /// the . Use this if you want specific registers to NOT be saved and restored, - /// either to improve performance or to modify the behavior of the original code. - public HookOptions(HookExecutionMode executionMode, HookIsolationMode isolationMode, - params Register[] registersExcludedFromPreservation) - : this(executionMode, isolationMode, HookJumpMode.NearJumpWithFallbackOnFarJump, - registersExcludedFromPreservation) { } + /// Optional registers to save before the injected code is executed, and restore + /// after it is executed. Use this to isolate the injected code from the original code, to prevent it from affecting + /// the original code's behavior or causing crashes. + public HookOptions(HookExecutionMode executionMode, params HookRegister[] registersToPreserve) + : this(executionMode, HookJumpMode.NearJumpWithFallbackOnFarJump, registersToPreserve) { } /// /// Initializes a new instance of the class with the given values. /// /// Execution mode of the injected code in relation to the original code. Defines /// whether the original code should be overwritten, executed before, or executed after the injected code. - /// Isolation mode flags defining how the injected code is isolated from the original - /// code. Depending on the mode, instructions may be prepended and appended to the injected code to save and - /// restore registers and flags, allowing the injected code to run without affecting the original code. /// Jump mode, which defines what kind of jump should be used to redirect the code flow to /// the injected code. Use , unless performance is critical /// and a far jump would be unacceptable. - /// Optional registers to exclude from preservation, regardless of - /// the . Use this if you want specific registers to NOT be saved and restored, - /// either to improve performance or to modify the behavior of the original code. - public HookOptions(HookExecutionMode executionMode, HookIsolationMode isolationMode, HookJumpMode jumpMode, - params Register[] registersExcludedFromPreservation) + /// Optional registers to save before the injected code is executed, and restore + /// after it is executed. Use this to isolate the injected code from the original code, to prevent it from affecting + /// the original code behavior or causing crashes. + public HookOptions(HookExecutionMode executionMode, HookJumpMode jumpMode, + params HookRegister[] registersToPreserve) { ExecutionMode = executionMode; - IsolationMode = isolationMode; JumpMode = jumpMode; - RegistersExcludedFromPreservation = registersExcludedFromPreservation; - _registersToPreserve = GetRegistersToPreserve(); - } - - private readonly Register[] _registersToPreserve; - - /// - /// Gets the registers to save and restore, based on the and - /// properties. - /// - internal IEnumerable RegistersToPreserve => _registersToPreserve; - - /// - /// Builds the array of registers to save and restore, based on the and - /// properties. - /// - private Register[] GetRegistersToPreserve() - { - List registersToPreserve = new(64); - if (IsolationMode.HasFlag(HookIsolationMode.PreserveGeneralPurposeRegisters)) - registersToPreserve.AddRange(PreservableGeneralPurposeRegisters); - if (IsolationMode.HasFlag(HookIsolationMode.PreserveXmmRegisters)) - registersToPreserve.AddRange(XmmRegisters); - if (IsolationMode.HasFlag(HookIsolationMode.PreserveMmRegisters)) - registersToPreserve.AddRange(MmRegisters); - - return registersToPreserve.Except(RegistersExcludedFromPreservation).ToArray(); + RegistersToPreserve = registersToPreserve; } /// /// Gets the predicted length in bytes of the additional code that will be prepended and appended to the hook code. /// - /// Indicates if the target process is 64-bit. + /// Indicates if the target process is 64-bit. /// The total size in bytes of the additional code. - internal int GetExpectedGeneratedCodeSize(bool is64Bits) + internal int GetExpectedGeneratedCodeSize(bool is64Bit) { int totalSize = 0; - if (IsolationMode.HasFlag(HookIsolationMode.PreserveFlags)) - totalSize += AssemblerExtensions.GetSizeOfFlagsSave(is64Bits) - + AssemblerExtensions.GetSizeOfFlagsRestore(is64Bits); + if (RegistersToPreserve.Contains(HookRegister.Flags)) + totalSize += AssemblerExtensions.GetSizeOfFlagsSave(is64Bit) + + AssemblerExtensions.GetSizeOfFlagsRestore(is64Bit); - foreach (var register in RegistersToPreserve) + foreach (var register in RegistersToPreserve.Select(r => r.ToRegister(is64Bit)).Where(r => r != null)) { - totalSize += AssemblerExtensions.GetSizeOfSaveInstructions(register, is64Bits); - totalSize += AssemblerExtensions.GetSizeOfRestoreInstructions(register, is64Bits); + totalSize += AssemblerExtensions.GetSizeOfSaveInstructions(register!.Value, is64Bit); + totalSize += AssemblerExtensions.GetSizeOfRestoreInstructions(register.Value, is64Bit); } - if (IsolationMode.HasFlag(HookIsolationMode.PreserveFpuStack)) - totalSize += AssemblerExtensions.GetSizeOfFpuStackSave(is64Bits) - + AssemblerExtensions.GetSizeOfFpuStackRestore(is64Bits); + if (RegistersToPreserve.Contains(HookRegister.FpuStack)) + totalSize += AssemblerExtensions.GetSizeOfFpuStackSave(is64Bit) + + AssemblerExtensions.GetSizeOfFpuStackRestore(is64Bit); return totalSize; } diff --git a/src/MindControl.Code/Hooks/HookRegister.cs b/src/MindControl.Code/Hooks/HookRegister.cs new file mode 100644 index 0000000..8c414dc --- /dev/null +++ b/src/MindControl.Code/Hooks/HookRegister.cs @@ -0,0 +1,240 @@ +using Iced.Intel; + +namespace MindControl.Hooks; + +/// +/// Registers or register groups that can be saved and restored in hooks. +/// +public enum HookRegister +{ + /// Represents the RFLAGS (in 64-bit) or EFLAGS (in 32-bit) register, holding CPU flags. + Flags, + /// Represents the RAX (in 64-bit) or EAX (in 32-bit) register. + RaxEax, + /// Represents the RBX (in 64-bit) or EBX (in 32-bit) register. + RbxEbx, + /// Represents the RCX (in 64-bit) or ECX (in 32-bit) register. + RcxEcx, + /// Represents the RDX (in 64-bit) or EDX (in 32-bit) register. + RdxEdx, + /// Represents the RSI (in 64-bit) or ESI (in 32-bit) register. + RsiEsi, + /// Represents the RDI (in 64-bit) or EDI (in 32-bit) register. + RdiEdi, + /// Represents the RBP (in 64-bit) or EBP (in 32-bit) register. + RbpEbp, + /// Represents the RSP (in 64-bit) or ESP (in 32-bit) register. + RspEsp, + /// Represents the R8 register (64-bit only). + R8, + /// Represents the R9 register (64-bit only). + R9, + /// Represents the R10 register (64-bit only). + R10, + /// Represents the R11 register (64-bit only). + R11, + /// Represents the R12 register (64-bit only). + R12, + /// Represents the R13 register (64-bit only). + R13, + /// Represents the R14 register (64-bit only). + R14, + /// Represents the R15 register (64-bit only). + R15, + /// Represents the XMM0 register. + Xmm0, + /// Represents the XMM1 register. + Xmm1, + /// Represents the XMM2 register. + Xmm2, + /// Represents the XMM3 register. + Xmm3, + /// Represents the XMM4 register. + Xmm4, + /// Represents the XMM5 register. + Xmm5, + /// Represents the XMM6 register. + Xmm6, + /// Represents the XMM7 register. + Xmm7, + /// Represents the XMM8 register (64-bit only). + Xmm8, + /// Represents the XMM9 register (64-bit only). + Xmm9, + /// Represents the XMM10 register (64-bit only). + Xmm10, + /// Represents the XMM11 register (64-bit only). + Xmm11, + /// Represents the XMM12 register (64-bit only). + Xmm12, + /// Represents the XMM13 register (64-bit only). + Xmm13, + /// Represents the XMM14 register (64-bit only). + Xmm14, + /// Represents the XMM15 register (64-bit only). + Xmm15, + /// Represents the MM0 register. This register is not normally used. It is shared with the FPU stack. Do + /// not use both at the same time. + Mm0, + /// Represents the MM1 register. This register is not normally used. It is shared with the FPU stack. Do + /// not use both at the same time. + Mm1, + /// Represents the MM2 register. This register is not normally used. It is shared with the FPU stack. Do + /// not use both at the same time. + Mm2, + /// Represents the MM3 register. This register is not normally used. It is shared with the FPU stack. Do + /// not use both at the same time. + Mm3, + /// Represents the MM4 register. This register is not normally used. It is shared with the FPU stack. Do + /// not use both at the same time. + Mm4, + /// Represents the MM5 register. This register is not normally used. It is shared with the FPU stack. Do + /// not use both at the same time. + Mm5, + /// Represents the MM6 register. This register is not normally used. It is shared with the FPU stack. Do + /// not use both at the same time. + Mm6, + /// Represents the MM7 register. This register is not normally used. It is shared with the FPU stack. Do + /// not use both at the same time. + Mm7, + /// Represents all ST registers (ST0-ST7). Since they work as a stack, the whole stack is saved and + /// restored coordinatedly. These registers are not normally used. They are shared with the MMX registers (MM0-MM7). + /// Do not use both at the same time. + FpuStack +} + +/// +/// Provides preset collections of registers for hooks. +/// +public static class HookRegisters +{ + /// + /// Use this collection to preserve flags and general-purpose registers. Works for both 32-bit and 64-bit processes + /// (incompatible registers are filtered out). This collection includes RFLAGS/EFLAGS, RAX/EAX, RBX/EBX, RCX/ECX, + /// RDX/EDX, RSI/ESI, RDI/EDI, RBP/EBP, RSP/ESP, and R8-R15 for 64-bit processes. It does not include the XMM + /// registers commonly used for floating-point operations. + /// + public static readonly HookRegister[] GeneralPurposeRegisters = + [ + HookRegister.Flags, + HookRegister.RaxEax, + HookRegister.RbxEbx, + HookRegister.RcxEcx, + HookRegister.RdxEdx, + HookRegister.RsiEsi, + HookRegister.RdiEdi, + HookRegister.RbpEbp, + HookRegister.RspEsp, + HookRegister.R8, + HookRegister.R9, + HookRegister.R10, + HookRegister.R11, + HookRegister.R12, + HookRegister.R13, + HookRegister.R14, + HookRegister.R15 + ]; + + /// + /// Use this collection to preserve XMM registers. Works for both 32-bit and 64-bit processes (incompatible + /// registers are filtered out). This collection includes all XMM registers, from XMM0 to XMM15 (XMM8 to XMM15 are + /// only available in 64-bit processes). + /// + public static readonly HookRegister[] XmmRegisters = + [ + HookRegister.Xmm0, + HookRegister.Xmm1, + HookRegister.Xmm2, + HookRegister.Xmm3, + HookRegister.Xmm4, + HookRegister.Xmm5, + HookRegister.Xmm6, + HookRegister.Xmm7, + HookRegister.Xmm8, + HookRegister.Xmm9, + HookRegister.Xmm10, + HookRegister.Xmm11, + HookRegister.Xmm12, + HookRegister.Xmm13, + HookRegister.Xmm14, + HookRegister.Xmm15 + ]; + + /// + /// Use this collection to preserve MMX registers. Works for both 32-bit and 64-bit processes. This collection + /// includes all MM registers, from MM0 to MM7. These registers are not normally used. They are shared with the FPU + /// stack. Do not use both at the same time. + /// + public static readonly HookRegister[] MmRegisters = + [ + HookRegister.Mm0, + HookRegister.Mm1, + HookRegister.Mm2, + HookRegister.Mm3, + HookRegister.Mm4, + HookRegister.Mm5, + HookRegister.Mm6, + HookRegister.Mm7 + ]; + + /// + /// Use this collection to preserve all commonly used registers. Works for both 32-bit and 64-bit processes + /// (incompatible registers are filtered out). This collection includes all general-purpose registers and XMM + /// registers. This is a good choice if you want to isolate the injected code from the original code, and are not + /// sure which registers the injected code will use (e.g. when calling an injected DLL function). + /// + public static readonly HookRegister[] AllCommonRegisters = GeneralPurposeRegisters.Concat(XmmRegisters).ToArray(); + + /// + /// Converts a to a if possible and when compatible with the + /// specified bitness. + /// + /// Hook register to convert. + /// Indicates if the target process bitness is 64-bit (true) or 32-bit (false). + /// The equivalent if the conversion is possible and compatible with the bitness, + /// or null otherwise. + internal static Register? ToRegister(this HookRegister hookRegister, bool is64Bit) => hookRegister switch + { + HookRegister.RaxEax => is64Bit ? Register.RAX : Register.EAX, + HookRegister.RbxEbx => is64Bit ? Register.RBX : Register.EBX, + HookRegister.RcxEcx => is64Bit ? Register.RCX : Register.ECX, + HookRegister.RdxEdx => is64Bit ? Register.RDX : Register.EDX, + HookRegister.RsiEsi => is64Bit ? Register.RSI : Register.ESI, + HookRegister.RdiEdi => is64Bit ? Register.RDI : Register.EDI, + HookRegister.RbpEbp => is64Bit ? Register.RBP : Register.EBP, + HookRegister.RspEsp => is64Bit ? Register.RSP : Register.ESP, + HookRegister.R8 => is64Bit ? Register.R8 : null, + HookRegister.R9 => is64Bit ? Register.R9 : null, + HookRegister.R10 => is64Bit ? Register.R10 : null, + HookRegister.R11 => is64Bit ? Register.R11 : null, + HookRegister.R12 => is64Bit ? Register.R12 : null, + HookRegister.R13 => is64Bit ? Register.R13 : null, + HookRegister.R14 => is64Bit ? Register.R14 : null, + HookRegister.R15 => is64Bit ? Register.R15 : null, + HookRegister.Xmm0 => Register.XMM0, + HookRegister.Xmm1 => Register.XMM1, + HookRegister.Xmm2 => Register.XMM2, + HookRegister.Xmm3 => Register.XMM3, + HookRegister.Xmm4 => Register.XMM4, + HookRegister.Xmm5 => Register.XMM5, + HookRegister.Xmm6 => Register.XMM6, + HookRegister.Xmm7 => Register.XMM7, + HookRegister.Xmm8 => is64Bit ? Register.XMM8 : null, + HookRegister.Xmm9 => is64Bit ? Register.XMM9 : null, + HookRegister.Xmm10 => is64Bit ? Register.XMM10 : null, + HookRegister.Xmm11 => is64Bit ? Register.XMM11 : null, + HookRegister.Xmm12 => is64Bit ? Register.XMM12 : null, + HookRegister.Xmm13 => is64Bit ? Register.XMM13 : null, + HookRegister.Xmm14 => is64Bit ? Register.XMM14 : null, + HookRegister.Xmm15 => is64Bit ? Register.XMM15 : null, + HookRegister.Mm0 => Register.MM0, + HookRegister.Mm1 => Register.MM1, + HookRegister.Mm2 => Register.MM2, + HookRegister.Mm3 => Register.MM3, + HookRegister.Mm4 => Register.MM4, + HookRegister.Mm5 => Register.MM5, + HookRegister.Mm6 => Register.MM6, + HookRegister.Mm7 => Register.MM7, + _ => null + }; +} \ No newline at end of file diff --git a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs index 5f9405a..73c5136 100644 --- a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs +++ b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs @@ -43,7 +43,7 @@ public static class ProcessMemoryHookExtensions public static Result Hook(this ProcessMemory processMemory, UIntPtr targetInstructionAddress, byte[] injectedCode, HookOptions options) { - ulong sizeToReserve = (ulong)(options.GetExpectedGeneratedCodeSize(processMemory.Is64Bits) + ulong sizeToReserve = (ulong)(options.GetExpectedGeneratedCodeSize(processMemory.Is64Bit) + injectedCode.Length + MaxInstructionLength // Extra room for the original instructions + FarJumpInstructionLength); // Extra room for the jump back to the original code @@ -64,7 +64,7 @@ public static Result Hook(this ProcessMemory processMemor // Assemble the jump to the injected code var reservation = reservationResult.Value; - var jmpAssembler = new Assembler(processMemory.Is64Bits ? 64 : 32); + var jmpAssembler = new Assembler(processMemory.Is64Bit ? 64 : 32); jmpAssembler.jmp(reservation.Address); var jmpAssembleResult = jmpAssembler.AssembleToBytes(targetInstructionAddress); if (jmpAssembleResult.IsFailure) @@ -78,7 +78,7 @@ public static Result Hook(this ProcessMemory processMemor // Read the original instructions to replace, until we have enough bytes to fit the jump to the injected code using var stream = processMemory.GetMemoryStream(targetInstructionAddress); var codeReader = new StreamCodeReader(stream); - var decoder = Decoder.Create(processMemory.Is64Bits ? 64 : 32, codeReader, targetInstructionAddress); + var decoder = Decoder.Create(processMemory.Is64Bit ? 64 : 32, codeReader, targetInstructionAddress); var instructionsToReplace = new List(); int bytesRead = 0; while (bytesRead < jmpBytes.Length) @@ -112,7 +112,7 @@ public static Result Hook(this ProcessMemory processMemor var nextOriginalInstructionAddress = (UIntPtr)(targetInstructionAddress + (ulong)bytesRead); // Assemble the pre-code - var preHookCodeResult = BuildPreHookCode(reservation.Address, instructionsToReplace, processMemory.Is64Bits, + var preHookCodeResult = BuildPreHookCode(reservation.Address, instructionsToReplace, processMemory.Is64Bit, options); if (preHookCodeResult.IsFailure) { @@ -124,7 +124,7 @@ public static Result Hook(this ProcessMemory processMemor // Assemble the post-code var postHookCodeAddress = (UIntPtr)(reservation.Address + (ulong)preHookCodeBytes.Length + (ulong)injectedCode.Length); - var postHookCodeResult = BuildPostHookCode(postHookCodeAddress, instructionsToReplace, processMemory.Is64Bits, + var postHookCodeResult = BuildPostHookCode(postHookCodeAddress, instructionsToReplace, processMemory.Is64Bit, options, nextOriginalInstructionAddress); if (postHookCodeResult.IsFailure) { @@ -171,13 +171,13 @@ public static Result Hook(this ProcessMemory processMemor /// /// Address where the pre-hook code is going to be written. /// Original instructions to be replaced by a jump to the injected code. - /// Boolean indicating if the target process is 64-bits. + /// Boolean indicating if the target process is 64-bit. /// Options defining how the hook behaves. /// A result holding either the assembled code bytes, or a hook failure. private static Result BuildPreHookCode(ulong baseAddress, - IList instructionsToReplace, bool is64Bits, HookOptions options) + IList instructionsToReplace, bool is64Bit, HookOptions options) { - var assembler = new Assembler(is64Bits ? 64 : 32); + var assembler = new Assembler(is64Bit ? 64 : 32); // If the hook mode specifies that the original instruction should be executed first, add it to the code if (options.ExecutionMode == HookExecutionMode.ExecuteOriginalInstructionFirst @@ -187,21 +187,23 @@ private static Result BuildPreHookCode(ulong baseAddress, } // Save flags if needed - if (options.IsolationMode.HasFlag(HookIsolationMode.PreserveFlags)) + if (options.RegistersToPreserve.Contains(HookRegister.Flags)) assembler.SaveFlags(); // Save registers if needed - foreach (var register in options.RegistersToPreserve) + foreach (var hookRegister in options.RegistersToPreserve) { + var register = hookRegister.ToRegister(is64Bit); + // Skip the register if it's not supported or not compatible with the target architecture - if (!register.IsIndividualPreservationSupported(is64Bits)) + if (register == null) continue; - assembler.SaveRegister(register); + assembler.SaveRegister(register.Value); } // Save the FPU stack if needed - if (options.IsolationMode.HasFlag(HookIsolationMode.PreserveFpuStack)) + if (options.RegistersToPreserve.Contains(HookRegister.FpuStack)) assembler.SaveFpuStack(); // Assemble the code and return the resulting bytes @@ -217,31 +219,33 @@ private static Result BuildPreHookCode(ulong baseAddress, /// /// Address where the post-hook code is going to be written. /// Original instructions to be replaced by a jump to the injected code. - /// Boolean indicating if the target process is 64-bits. + /// Boolean indicating if the target process is 64-bit. /// Options defining how the hook behaves. /// Address of the first byte of the instruction to jump back to. /// A result holding either the assembled code bytes, or a hook failure. private static Result BuildPostHookCode(ulong baseAddress, - IList instructionsToReplace, bool is64Bits, HookOptions options, UIntPtr originalCodeJumpTarget) + IList instructionsToReplace, bool is64Bit, HookOptions options, UIntPtr originalCodeJumpTarget) { - var assembler = new Assembler(is64Bits ? 64 : 32); + var assembler = new Assembler(is64Bit ? 64 : 32); // Restore the FPU stack if needed - if (options.IsolationMode.HasFlag(HookIsolationMode.PreserveFpuStack)) + if (options.RegistersToPreserve.Contains(HookRegister.FpuStack)) assembler.RestoreFpuStack(); // Restore registers if needed - foreach (var register in options.RegistersToPreserve.Reverse()) + foreach (var hookRegister in options.RegistersToPreserve.Reverse()) { + var register = hookRegister.ToRegister(is64Bit); + // Skip the register if it's not supported or not compatible with the target architecture - if (!register.IsIndividualPreservationSupported(is64Bits)) + if (register == null) continue; - assembler.RestoreRegister(register); + assembler.RestoreRegister(register.Value); } // Restore flags if needed - if (options.IsolationMode.HasFlag(HookIsolationMode.PreserveFlags)) + if (options.RegistersToPreserve.Contains(HookRegister.Flags)) assembler.RestoreFlags(); // If the hook mode specifies that the hook code should be executed first, append the original instructions @@ -279,17 +283,17 @@ private static Result ReserveNearHookTarge // The range of a near jump is limited by the signed byte displacement that follows the opcode. // The displacement is a signed 4-byte integer, so the range is from -2GB to +2GB. - // In 32-bits processes, near jumps can be made to any address, despite the offset being a signed integer. + // In 32-bit processes, near jumps can be made to any address, despite the offset being a signed integer. // For example, a jump from 0x00000000 to 0xFFFFFFFF can be made with a near jump with an offset of -1. // Which means that for 32-bit processes, we can reserve memory anywhere in the address space. - if (!processMemory.Is64Bits) + if (!processMemory.Is64Bit) return processMemory.Reserve(sizeToReserve, true, nearAddress: jumpAddress); - // In 64-bits processes, however, the offset is still a signed 4-byte integer, but the full range is much + // In 64-bit processes, however, the offset is still a signed 4-byte integer, but the full range is much // larger, meaning we can't reach any address. - // For practical purposes, we disregard wrap-around jumps for 64-bits processes (cases where the address plus - // the offset is outside the 64-bits address space), because they mostly make sense for 32-bits processes and + // For practical purposes, we disregard wrap-around jumps for 64-bit processes (cases where the address plus + // the offset is outside the 64-bit address space), because they mostly make sense for 32-bit processes and // would complicate things unnecessarily. // The displacement is relative to the address of the next instruction. A near jump instruction is 5 bytes long. diff --git a/src/MindControl/Addressing/MemoryRange.cs b/src/MindControl/Addressing/MemoryRange.cs index b3b852c..48cddb4 100644 --- a/src/MindControl/Addressing/MemoryRange.cs +++ b/src/MindControl/Addressing/MemoryRange.cs @@ -11,10 +11,10 @@ public readonly record struct MemoryRange /// End address of the range. public UIntPtr End { get; init; } - /// Instance representing the full 32-bits address space. - /// There is no 64-bits equivalent because, if the system is 32-bits, such a range cannot be represented. + /// Instance representing the full 32-bit address space. + /// There is no 64-bit equivalent because, if the system is 32-bit, such a range cannot be represented. /// - public static readonly MemoryRange Full32BitsRange = new(UIntPtr.Zero, (UIntPtr)int.MaxValue); + public static readonly MemoryRange Full32BitRange = new(UIntPtr.Zero, int.MaxValue); /// /// Builds a . @@ -134,7 +134,8 @@ public IEnumerable Exclude(MemoryRange rangeToExclude) /// Returns a subset of this range, aligned to the specified byte alignment. /// For example, a range of [2,9] aligned to 4 bytes will result in [4,8]. /// - /// Alignment in bytes. Usually 4 for 32-bits processes, or 8 for 64-bits processes. + /// Alignment in bytes. Usually 4 for 32-bit processes, or 8 for 64-bit processes. + /// An alignment of 4 or 8 usually provide better performance when reading or writing memory. /// Alignment mode. Defines how the range should be aligned. Defaults to /// . /// The aligned memory range. The returned range is always a subset of the range, or the range itself. diff --git a/src/MindControl/Addressing/PointerOffset.cs b/src/MindControl/Addressing/PointerOffset.cs index da76a81..654fbd6 100644 --- a/src/MindControl/Addressing/PointerOffset.cs +++ b/src/MindControl/Addressing/PointerOffset.cs @@ -14,9 +14,9 @@ public readonly record struct PointerOffset(ulong Offset, bool IsNegative) public static readonly PointerOffset Zero = new(0, false); /// - /// Gets a value indicating whether this offset is 64-bits. + /// Gets a value indicating whether this offset is 64-bit. /// - public bool Is64Bits => Offset > uint.MaxValue; + public bool Is64Bit => Offset > uint.MaxValue; /// /// Produces the result of the addition between this offset and the given value. diff --git a/src/MindControl/Addressing/PointerPath.cs b/src/MindControl/Addressing/PointerPath.cs index de4cc00..3f61114 100644 --- a/src/MindControl/Addressing/PointerPath.cs +++ b/src/MindControl/Addressing/PointerPath.cs @@ -34,10 +34,10 @@ private readonly struct ExpressionParsedData public PointerOffset[] PointerOffsets { get; init; } /// - /// Gets a boolean indicating if the path is a 64-bit only path, or if it can also be used in a 32-bits + /// Gets a boolean indicating if the path is a 64-bit only path, or if it can also be used in a 32-bit /// process. /// - public bool IsStrictly64Bits => BaseModuleOffset.Is64Bits || PointerOffsets.Any(offset => offset.Is64Bits); + public bool IsStrictly64Bit => BaseModuleOffset.Is64Bit || PointerOffsets.Any(offset => offset.Is64Bit); } /// @@ -69,13 +69,13 @@ private readonly struct ExpressionParsedData public PointerOffset[] PointerOffsets => _parsedData.PointerOffsets; /// - /// Gets a boolean indicating if the path is a 64-bit only path, or if it can also be used in a 32-bits + /// Gets a boolean indicating if the path is a 64-bit only path, or if it can also be used in a 32-bit /// process. /// For example, for the expression "app.dll+0000F04AA1218410", this boolean would be True. /// For the expression "app.dll+00000000F04AA121", this boolean would be False. /// Note that evaluating a 32-bit-compatible address may still end up overflowing. /// - public bool IsStrictly64Bits => _parsedData.IsStrictly64Bits; + public bool IsStrictly64Bit => _parsedData.IsStrictly64Bit; /// /// Builds a pointer path from the given expression. @@ -111,13 +111,13 @@ private PointerPath(string expression, ExpressionParsedData? parsedData) /// Checks the given expression and returns a boolean indicating if it is valid or not. /// /// Expression to check. - /// If set to True, valid 64-bit expressions will still cause False to be returned. + /// If set to True, valid 64-bit expressions will still cause False to be returned. /// /// True if the expression is valid. - public static bool IsValid(string expression, bool allowOnly32Bits = false) + public static bool IsValid(string expression, bool allowOnly32Bit = false) { var parsedData = Parse(expression); - return parsedData != null && (!allowOnly32Bits || !parsedData.Value.IsStrictly64Bits); + return parsedData != null && (!allowOnly32Bit || !parsedData.Value.IsStrictly64Bit); } /// @@ -125,14 +125,14 @@ public static bool IsValid(string expression, bool allowOnly32Bits = false) /// expression was successfully parsed, or null if the expression is not valid. /// /// Expression to check. - /// If set to True, valid 64-bit expressions will still cause null to be returned. + /// If set to True, valid 64-bit expressions will still cause null to be returned. /// /// The resulting instance if the expression was successfully parsed, or /// null if the expression is not valid. - public static PointerPath? TryParse(string expression, bool allowOnly32Bits = false) + public static PointerPath? TryParse(string expression, bool allowOnly32Bit = false) { var parsedData = Parse(expression); - return parsedData == null || allowOnly32Bits && parsedData.Value.IsStrictly64Bits ? + return parsedData == null || allowOnly32Bit && parsedData.Value.IsStrictly64Bit ? null : new PointerPath(expression, parsedData); } diff --git a/src/MindControl/Allocation/MemoryAllocation.cs b/src/MindControl/Allocation/MemoryAllocation.cs index e17aabf..dd21c5a 100644 --- a/src/MindControl/Allocation/MemoryAllocation.cs +++ b/src/MindControl/Allocation/MemoryAllocation.cs @@ -113,7 +113,8 @@ private IEnumerable GetFreeRanges() /// Optional byte alignment for the range. When null, values are not aligned. The /// default value is 8, meaning that for example a range of [0x15,0x3C] will be aligned to [0x18,0x38] and thus /// only accomodate 32 bytes. Alignment means the resulting range might be bigger than the , - /// but will never make it smaller. + /// but will never make it smaller. An alignment of 4 or 8 usually provide better performance when reading or + /// writing memory. /// The first free range that can fit the specified size, or null if no range is large enough. public MemoryRange? GetNextRangeFittingSize(ulong size, uint? byteAlignment = 8) { diff --git a/src/MindControl/Internal/ConversionExtensions.cs b/src/MindControl/Internal/ConversionExtensions.cs index f966283..0a621d4 100644 --- a/src/MindControl/Internal/ConversionExtensions.cs +++ b/src/MindControl/Internal/ConversionExtensions.cs @@ -43,12 +43,12 @@ public static ulong ReadUnsignedNumber(this Span bytes) /// Converts the pointer to an array of bytes. /// /// Pointer to convert. - /// A boolean value indicating if the target architecture of the pointer is 64-bits or not. + /// A boolean value indicating if the target architecture of the pointer is 64-bit or not. /// This value is used to determine the size of the returned array. /// The array of bytes representing the pointer. - public static byte[] ToBytes(this IntPtr pointer, bool is64) + public static byte[] ToBytes(this IntPtr pointer, bool is64Bit) { - var result = new byte[is64 ? 8 : 4]; + var result = new byte[is64Bit ? 8 : 4]; Marshal.Copy(pointer, result, 0, result.Length); return result; } diff --git a/src/MindControl/Native/IOperatingSystemService.cs b/src/MindControl/Native/IOperatingSystemService.cs index 3c09fff..df54f54 100644 --- a/src/MindControl/Native/IOperatingSystemService.cs +++ b/src/MindControl/Native/IOperatingSystemService.cs @@ -18,8 +18,8 @@ public interface IOperatingSystemService /// Returns a value indicating if the process with the given identifier is a 64-bit process or not. /// /// Identifier of the target process. - /// A result holding either a boolean indicating if the process is 64-bits, or a system failure. - Result IsProcess64Bits(int pid); + /// A result holding either a boolean indicating if the process is 64-bit, or a system failure. + Result IsProcess64Bit(int pid); /// /// Reads a targeted range of the memory of a specified process. @@ -53,14 +53,14 @@ Result ReadProcessMemoryPartial(IntPtr processHandle, UInt /// /// Handle of the target process. /// The handle must have PROCESS_VM_OPERATION access. - /// A boolean indicating if the target process is 64 bits or not. + /// A boolean indicating if the target process is 64-bit or not. /// An address in the target page. /// New protection value for the page. /// A result holding either the memory protection value that was effective on the page before being /// changed, or a system failure. /// The process handle is invalid (zero pointer). /// The target address is invalid (zero pointer). - Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bits, + Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bit, UIntPtr targetAddress, MemoryProtection newProtection); /// @@ -147,11 +147,10 @@ Result CreateRemoteThread(IntPtr processHandle, UIntPtr s /// /// Handle of the target process. /// Base address of the target memory region. - /// A boolean indicating if the target process is 64 bits or not. - /// If left null, the method will automatically determine the bitness of the process. + /// A boolean indicating if the target process is 64-bit or not. /// A result holding either the metadata of the target memory region, or a system failure. Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress, - bool is64Bits); + bool is64Bit); /// /// Gets the allocation granularity (minimal allocation size) of the system. diff --git a/src/MindControl/Native/Win32Service.cs b/src/MindControl/Native/Win32Service.cs index 134e2d8..d98758f 100644 --- a/src/MindControl/Native/Win32Service.cs +++ b/src/MindControl/Native/Win32Service.cs @@ -39,8 +39,8 @@ public Result OpenProcess(int pid) /// Returns a value indicating if the process with the given identifier is a 64-bit process or not. /// /// Identifier of the target process. - /// A result holding either a boolean indicating if the process is 64-bits, or a system failure. - public Result IsProcess64Bits(int pid) + /// A result holding either a boolean indicating if the process is 64-bit, or a system failure. + public Result IsProcess64Bit(int pid) { try { @@ -48,8 +48,8 @@ public Result IsProcess64Bits(int pid) if (!IsWow64Process(process.Handle, out bool isWow64)) return GetLastSystemError(); - // Process is 64 bits if we are running a 64-bits system and the process is NOT in wow64. - return !isWow64 && IsSystem64Bits(); + // Process is 64-bit if we are running a 64-bit system and the process is NOT in wow64. + return !isWow64 && IsSystem64Bit(); } catch (Exception) { @@ -59,10 +59,10 @@ public Result IsProcess64Bits(int pid) } /// - /// Returns a value indicating if the system is 64 bits or not. + /// Returns a value indicating if the system is 64-bit or not. /// - /// A boolean indicating if the system is 64 bits or not. - private bool IsSystem64Bits() => IntPtr.Size == 8; + /// A boolean indicating if the system is 64-bit or not. + private bool IsSystem64Bit() => IntPtr.Size == 8; /// /// Reads a targeted range of the memory of a specified process. @@ -147,16 +147,16 @@ public Result ReadProcessMemoryPartial(IntPtr processHandl if (length == 1) return initialReadError; - // Determine if the process is 64 bits + // Determine if the process is 64-bit if (!IsWow64Process(processHandle, out bool isWow64)) return GetLastSystemError(); - bool is64Bits = !isWow64 && IsSystem64Bits(); + bool is64Bit = !isWow64 && IsSystem64Bit(); // Build the memory range that spans across everything we attempted to read var range = MemoryRange.FromStartAndSize(baseAddress, length); // Determine the last readable address within the range - var lastReadableAddress = GetLastConsecutiveReadableAddressWithinRange(processHandle, is64Bits, range); + var lastReadableAddress = GetLastConsecutiveReadableAddressWithinRange(processHandle, is64Bit, range); // If we couldn't determine the last readable address, or it matches/exceeds the end of the range, there // is no point in trying to read again. Return the initial read error. @@ -186,11 +186,11 @@ public Result ReadProcessMemoryPartial(IntPtr processHandl /// the end address of the last consecutive readable region. If the start of the range is unreadable, returns null. /// /// Handle of the target process. - /// A boolean indicating if the target process is 64 bits or not. + /// A boolean indicating if the target process is 64-bit or not. /// Memory range to check for readable regions. /// The end address of the last consecutive readable region within the range, or null if the start of the /// range is unreadable. - private UIntPtr? GetLastConsecutiveReadableAddressWithinRange(IntPtr processHandle, bool is64Bits, + private UIntPtr? GetLastConsecutiveReadableAddressWithinRange(IntPtr processHandle, bool is64Bit, MemoryRange range) { var applicationMemoryLimit = GetFullMemoryRange().End; @@ -198,7 +198,7 @@ public Result ReadProcessMemoryPartial(IntPtr processHandl UIntPtr currentAddress = range.Start; while (currentAddress.ToUInt64() <= rangeEnd) { - var getRegionResult = GetRegionMetadata(processHandle, currentAddress, is64Bits); + var getRegionResult = GetRegionMetadata(processHandle, currentAddress, is64Bit); // If we failed to get the region metadata, stop iterating. if (getRegionResult.IsFailure) @@ -229,12 +229,12 @@ public Result ReadProcessMemoryPartial(IntPtr processHandl /// /// Handle of the target process. /// The handle must have PROCESS_VM_OPERATION access. - /// A boolean indicating if the target process is 64 bits or not. + /// A boolean indicating if the target process is 64-bit or not. /// An address in the target page. /// New protection value for the page. /// A result holding either the memory protection value that was effective on the page before being /// changed, or a system failure. - public Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bits, + public Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bit, UIntPtr targetAddress, MemoryProtection newProtection) { if (processHandle == IntPtr.Zero) @@ -244,7 +244,7 @@ public Result ReadAndOverwriteProtection(IntPtr return new SystemFailureOnInvalidArgument(nameof(targetAddress), "The target address cannot be a zero pointer."); - var result = VirtualProtectEx(processHandle, targetAddress, (IntPtr)(is64Bits ? 8 : 4), newProtection, + var result = VirtualProtectEx(processHandle, targetAddress, is64Bit ? 8 : 4, newProtection, out var previousProtection); return result ? previousProtection : GetLastSystemError(); @@ -450,13 +450,13 @@ public MemoryRange GetFullMemoryRange() /// /// Handle of the target process. /// Base address of the target memory region. - /// A boolean indicating if the target process is 64 bits or not. + /// A boolean indicating if the target process is 64-bit or not. /// A result holding either the metadata of the target memory region, or a system failure. public Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress, - bool is64Bits) + bool is64Bit) { MemoryBasicInformation memoryBasicInformation; - if (is64Bits) + if (is64Bit) { // Use the 64-bit variant of the structure. var memInfo64 = new MemoryBasicInformation64(); @@ -470,7 +470,7 @@ public Result GetRegionMetadata(IntPtr proce } else { - // Use the 32-bits variant of the structure. + // Use the 32-bit variant of the structure. var memInfo32 = new MemoryBasicInformation32(); if (VirtualQueryEx(processHandle, baseAddress, out memInfo32, (UIntPtr)Marshal.SizeOf(memInfo32)) == UIntPtr.Zero) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs index 6bb09e4..5eff37b 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs @@ -14,7 +14,7 @@ public partial class ProcessMemory /// The memory address pointed by the pointer path. public Result EvaluateMemoryAddress(PointerPath pointerPath) { - if (pointerPath.IsStrictly64Bits && (IntPtr.Size == 4 || !Is64Bits)) + if (pointerPath.IsStrictly64Bit && (IntPtr.Size == 4 || !Is64Bit)) return new PathEvaluationFailureOnIncompatibleBitness(); UIntPtr? baseAddress; diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index cac6e25..63701c9 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -97,7 +97,7 @@ private Result FindAndAllocateFreeMemory(ulong s MemoryRangeMetadata currentMetadata; while ((nextAddressForward.ToUInt64() < actualRange.Value.End.ToUInt64() || nextAddressBackwards.ToUInt64() > actualRange.Value.Start.ToUInt64()) - && (currentMetadata = _osService.GetRegionMetadata(ProcessHandle, nextAddress, Is64Bits) + && (currentMetadata = _osService.GetRegionMetadata(ProcessHandle, nextAddress, Is64Bit) .GetValueOrDefault()).Size.ToUInt64() > 0) { nextAddressForward = (UIntPtr)Math.Max(nextAddressForward.ToUInt64(), @@ -198,7 +198,7 @@ public Result Reserve(ulong size, bool req return new AllocationFailureOnInvalidArguments( "The size of the memory range to reserve must be greater than zero."); - uint alignment = Is64Bits ? (uint)8 : 4; + uint alignment = Is64Bit ? (uint)8 : 4; var existingAllocations = _allocations .Where(a => (!requireExecutable || a.IsExecutable) && (limitRange == null || limitRange.Value.Contains(a.Range.Start))); @@ -265,7 +265,7 @@ public Result Store(byte[] data, bool isCo /// A result holding either the reservation storing the data, or a reservation failure. public Result Store(byte[] data, MemoryAllocation allocation) { - uint alignment = Is64Bits ? (uint)8 : 4; + uint alignment = Is64Bit ? (uint)8 : 4; var reservedRangeResult = allocation.ReserveRange((ulong)data.Length, alignment); if (reservedRangeResult.IsFailure) return reservedRangeResult.Error; diff --git a/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs b/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs index 3986066..a279152 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs @@ -182,7 +182,7 @@ private MemoryRange[] GetAggregatedRegionRanges(MemoryRange range, FindBytesSett UIntPtr currentAddress = range.Start; while (currentAddress.ToUInt64() <= rangeEnd) { - var getRegionResult = _osService.GetRegionMetadata(ProcessHandle, currentAddress, Is64Bits); + var getRegionResult = _osService.GetRegionMetadata(ProcessHandle, currentAddress, Is64Bit); if (getRegionResult.IsFailure) { // If we failed to get the region metadata, we cannot continue because we don't know where the next diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs index 67c3823..e81128d 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs @@ -41,7 +41,7 @@ public Result WriteBytes(UIntPtr address, Span value, MemoryProtection? previousProtection = null; if (memoryProtectionStrategy is MemoryProtectionStrategy.Remove or MemoryProtectionStrategy.RemoveAndRestore) { - var protectionRemovalResult = _osService.ReadAndOverwriteProtection(ProcessHandle, Is64Bits, + var protectionRemovalResult = _osService.ReadAndOverwriteProtection(ProcessHandle, Is64Bit, address, MemoryProtection.ExecuteReadWrite); if (protectionRemovalResult.IsFailure) @@ -59,7 +59,7 @@ public Result WriteBytes(UIntPtr address, Span value, if (memoryProtectionStrategy == MemoryProtectionStrategy.RemoveAndRestore && previousProtection != MemoryProtection.ExecuteReadWrite) { - var protectionRestorationResult = _osService.ReadAndOverwriteProtection(ProcessHandle, Is64Bits, + var protectionRestorationResult = _osService.ReadAndOverwriteProtection(ProcessHandle, Is64Bit, address, previousProtection!.Value); if (protectionRestorationResult.IsFailure) @@ -208,7 +208,7 @@ private Result WriteUInt(UIntPtr address, uint value, /// A successful result, or a write failure private Result WriteIntPtr(UIntPtr address, IntPtr value, MemoryProtectionStrategy? memoryProtectionStrategy = null) - => WriteBytes(address, value.ToBytes(Is64Bits), memoryProtectionStrategy); + => WriteBytes(address, value.ToBytes(Is64Bit), memoryProtectionStrategy); /// /// Writes a float to the given address in the process memory. diff --git a/src/MindControl/ProcessMemory/ProcessMemory.cs b/src/MindControl/ProcessMemory/ProcessMemory.cs index cc504b7..96fdb94 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.cs @@ -26,9 +26,9 @@ public partial class ProcessMemory : IDisposable public bool IsAttached { get; private set; } /// - /// Gets a boolean indicating if the process is 64-bits. + /// Gets a boolean indicating if the process is 64-bit. /// - public bool Is64Bits { get; private set; } + public bool Is64Bit { get; private set; } /// /// Gets the handle of the attached process. @@ -139,9 +139,9 @@ private void Attach() { try { - Is64Bits = _osService.IsProcess64Bits(_process.Id).GetValueOrDefault(defaultValue: true); + Is64Bit = _osService.IsProcess64Bit(_process.Id).GetValueOrDefault(defaultValue: true); - if (Is64Bits && !Environment.Is64BitOperatingSystem) + if (Is64Bit && !Environment.Is64BitOperatingSystem) throw new ProcessException(_process.Id, "A 32-bit program cannot attach to a 64-bit process."); _process.EnableRaisingEvents = true; @@ -164,7 +164,7 @@ private void Attach() /// In other words, returns false if the pointer is a 64-bit address but the target process is 32-bit. /// /// Pointer to test. - private bool IsBitnessCompatible(UIntPtr pointer) => Is64Bits || pointer.ToUInt64() <= uint.MaxValue; + private bool IsBitnessCompatible(UIntPtr pointer) => Is64Bit || pointer.ToUInt64() <= uint.MaxValue; /// /// Gets a new instance of representing the attached process. diff --git a/src/MindControl/Results/PathEvaluationFailure.cs b/src/MindControl/Results/PathEvaluationFailure.cs index df006d4..1e3433d 100644 --- a/src/MindControl/Results/PathEvaluationFailure.cs +++ b/src/MindControl/Results/PathEvaluationFailure.cs @@ -6,7 +6,7 @@ public enum PathEvaluationFailureReason { /// - /// The target process is 32-bits, but the path is not compatible with a 32-bit address space. + /// The target process is 32-bit, but the path is not compatible with a 32-bit address space. /// IncompatibleBitness, @@ -33,7 +33,7 @@ public enum PathEvaluationFailureReason public abstract record PathEvaluationFailure(PathEvaluationFailureReason Reason); /// -/// Represents a failure in a path evaluation operation when the target process is 32-bits, but the target memory +/// Represents a failure in a path evaluation operation when the target process is 32-bit, but the target memory /// address is not within the 32-bit address space. /// /// Address where the value causing the issue was read. May be null if the first address @@ -44,7 +44,7 @@ public record PathEvaluationFailureOnIncompatibleBitness(UIntPtr? PreviousAddres /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => "The specified pointer path contains 64-bit offsets, but the target process is 32-bits."; + => "The specified pointer path contains 64-bit offsets, but the target process is 32-bit."; } /// diff --git a/src/MindControl/Results/ReadFailure.cs b/src/MindControl/Results/ReadFailure.cs index 0019853..cab9356 100644 --- a/src/MindControl/Results/ReadFailure.cs +++ b/src/MindControl/Results/ReadFailure.cs @@ -16,7 +16,7 @@ public enum ReadFailureReason PointerPathEvaluationFailure, /// - /// The target process is 32-bits, but the target memory address is not within the 32-bit address space. + /// The target process is 32-bit, but the target memory address is not within the 32-bit address space. /// IncompatibleBitness, @@ -73,7 +73,7 @@ public override string ToString() } /// -/// Represents a failure in a memory read operation when the target process is 32-bits, but the target memory address is +/// Represents a failure in a memory read operation when the target process is 32-bit, but the target memory address is /// not within the 32-bit address space. /// /// Address that caused the failure. @@ -83,7 +83,7 @@ public record ReadFailureOnIncompatibleBitness(UIntPtr Address) /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"The address to read, {Address}, is a 64-bit address, but the target process is 32-bits."; + => $"The address to read, {Address}, is a 64-bit address, but the target process is 32-bit."; } /// diff --git a/src/MindControl/Results/StringReadFailure.cs b/src/MindControl/Results/StringReadFailure.cs index 2c26fd4..cd78b8d 100644 --- a/src/MindControl/Results/StringReadFailure.cs +++ b/src/MindControl/Results/StringReadFailure.cs @@ -16,7 +16,7 @@ public enum StringReadFailureReason InvalidSettings, /// - /// The string read operation failed because the target process is 32-bits, but the target memory address is not + /// The string read operation failed because the target process is 32-bit, but the target memory address is not /// within the 32-bit address space. /// IncompatibleBitness, @@ -74,7 +74,7 @@ public override string ToString() } /// -/// Represents a failure in a string read operation when the target process is 32-bits, but the target memory address is +/// Represents a failure in a string read operation when the target process is 32-bit, but the target memory address is /// not within the 32-bit address space. /// /// Address that caused the failure. diff --git a/src/MindControl/Results/WriteFailure.cs b/src/MindControl/Results/WriteFailure.cs index 8370e0a..ec69b8e 100644 --- a/src/MindControl/Results/WriteFailure.cs +++ b/src/MindControl/Results/WriteFailure.cs @@ -11,7 +11,7 @@ public enum WriteFailureReason PointerPathEvaluationFailure, /// - /// The target process is 32-bits, but the target memory address is not within the 32-bit address space. + /// The target process is 32-bit, but the target memory address is not within the 32-bit address space. /// IncompatibleBitness, @@ -62,7 +62,7 @@ public override string ToString() } /// -/// Represents a failure in a memory write operation when the target process is 32-bits, but the target memory address +/// Represents a failure in a memory write operation when the target process is 32-bit, but the target memory address /// is not within the 32-bit address space. /// /// Address that caused the failure. @@ -72,7 +72,7 @@ public record WriteFailureOnIncompatibleBitness(UIntPtr Address) /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"The address to write, {Address}, is a 64-bit address, but the target process is 32-bits."; + => $"The address to write, {Address}, is a 64-bit address, but the target process is 32-bit."; } /// diff --git a/test/MindControl.Test/AddressingTests/PointerPathTest.cs b/test/MindControl.Test/AddressingTests/PointerPathTest.cs index cee263c..65cd4db 100644 --- a/test/MindControl.Test/AddressingTests/PointerPathTest.cs +++ b/test/MindControl.Test/AddressingTests/PointerPathTest.cs @@ -259,13 +259,13 @@ public readonly struct ExpressionTestCase { Expression = "mymoduleName.exe+FFFFFFFFFFFFFFFFF", ShouldBeValid = false, - Explanation = "A static offset cannot be over the 64-bits addressing limit." + Explanation = "A static offset cannot be over the 64-bit addressing limit." }, new ExpressionTestCase { Expression = "mymoduleName.exe+FFFFFFFFFFFFFFFF+1", ShouldBeValid = false, - Explanation = "A static offset cannot be over the 64-bits addressing limit after adding up." + Explanation = "A static offset cannot be over the 64-bit addressing limit after adding up." }, new ExpressionTestCase { @@ -289,12 +289,12 @@ public readonly struct ExpressionTestCase /// /// Tests for the given expression test case. - /// The allowOnly32Bits boolean parameter will be set to False. + /// The allowOnly32Bit boolean parameter will be set to False. /// Verifies that the result is the expected one. /// /// Target test case. [TestCaseSource(nameof(_testCases))] - public void IsValid_On64BitsTest(ExpressionTestCase testCase) + public void IsValidOn64BitTest(ExpressionTestCase testCase) { bool result = PointerPath.IsValid(testCase.Expression); Assert.That(result, Is.EqualTo(testCase.ShouldBeValid), testCase.Explanation); @@ -302,12 +302,12 @@ public void IsValid_On64BitsTest(ExpressionTestCase testCase) /// /// Tests for the given expression test case. - /// The allowOnly32Bits boolean parameter will be set to True. + /// The allowOnly32Bit boolean parameter will be set to True. /// Verifies that the result is the expected one, in accordance with the expected 64-bit exclusivity. /// /// Target test case. [TestCaseSource(nameof(_testCases))] - public void IsValid_On32BitsTest(ExpressionTestCase testCase) + public void IsValidOn32BitTest(ExpressionTestCase testCase) { bool result = PointerPath.IsValid(testCase.Expression, true); Assert.That(result, Is.EqualTo(testCase is { ShouldBeValid: true, Expect64BitOnly: false }), @@ -316,13 +316,13 @@ public void IsValid_On32BitsTest(ExpressionTestCase testCase) /// /// Tests for the given expression test case. - /// The allowOnly32Bits boolean parameter will be set to False. + /// The allowOnly32Bit boolean parameter will be set to False. /// Verifies that the result is null when the expression is expected to be invalid, or otherwise that each property /// of the resulting address matches expectations. /// /// Target test case. [TestCaseSource(nameof(_testCases))] - public void TryParse_On64BitsTest(ExpressionTestCase testCase) + public void TryParseOn64BitTest(ExpressionTestCase testCase) { var result = PointerPath.TryParse(testCase.Expression); AssertResultingAddress(result, testCase); @@ -330,13 +330,13 @@ public void TryParse_On64BitsTest(ExpressionTestCase testCase) /// /// Tests for the given expression test case. - /// The allowOnly32Bits boolean parameter will be set to True. + /// The allowOnly32Bit boolean parameter will be set to True. /// Verifies that the result is null when the expression is expected to be invalid in accordance with the expected /// 64-bit exclusivity, or otherwise that each property of the resulting address matches expectations. /// /// Target test case. [TestCaseSource(nameof(_testCases))] - public void TryParse_On32BitsTest(ExpressionTestCase testCase) + public void TryParseOn32BitTest(ExpressionTestCase testCase) { var result = PointerPath.TryParse(testCase.Expression, true); if (testCase is { ShouldBeValid: true, Expect64BitOnly: true }) @@ -399,7 +399,7 @@ private void AssertResultingAddress(PointerPath? result, ExpressionTestCase test Assert.That(result.BaseModuleName, Is.EqualTo(testCase.ExpectedModuleName), testCase.Explanation); Assert.That(result.BaseModuleOffset, Is.EqualTo(testCase.ExpectedModuleOffset), testCase.Explanation); Assert.That(result.PointerOffsets, Is.EquivalentTo(testCase.ExpectedPointerOffsets!), testCase.Explanation); - Assert.That(result.IsStrictly64Bits, Is.EqualTo(testCase.Expect64BitOnly), testCase.Explanation); + Assert.That(result.IsStrictly64Bit, Is.EqualTo(testCase.Expect64BitOnly), testCase.Explanation); }); } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs index 4f60299..4f53638 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs @@ -14,12 +14,11 @@ public class ProcessMemoryHookExtensionsTest : ProcessMemoryTest /// Tests the method. /// The hook replaces a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the /// long value in the target app, with a new MOV instruction that assigns a different value. - /// It is performed with full isolation, with the RAX register as an exception (because we want to interfere with - /// that register). + /// No registers are preserved. /// After hooking, we let the program run, and check that the output long value is the one written by the hook. /// [Test] - public void HookAndReplaceValueOfMovInstructionWithIsolationTest() + public void HookAndReplaceValueOfMovInstructionTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); @@ -28,7 +27,7 @@ public void HookAndReplaceValueOfMovInstructionWithIsolationTest() // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, - new HookOptions(HookExecutionMode.ReplaceOriginalInstruction, Register.RAX)); + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsSuccess, Is.True); Assert.That(hookResult.Value.InjectedCodeReservation, Is.Not.Null); @@ -46,26 +45,28 @@ public void HookAndReplaceValueOfMovInstructionWithIsolationTest() /// The hook targets a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the /// long value in the target app, and inserts a new MOV instruction that assigns a different value after the /// instruction. - /// It is performed with full isolation, without exceptions. + /// However, we specify that the should be preserved. /// After hooking, we let the program run, and check the output. The long value must be the expected, original one, - /// because the options specified that the registers should be isolated from the injected code. + /// because we specified that the RAX register should be isolated. /// [Test] - public void HookAndInsertValueOfMovInstructionWithCompleteIsolationTest() + public void HookAndInsertValueOfMovInstructionWithRegisterIsolationTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; + var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); - // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. + // Hook the instruction that writes the long value to RAX, to append code that writes another value. + // Specify that the RAX register should be isolated. var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, - new HookOptions(HookExecutionMode.ExecuteOriginalInstructionFirst)); + new HookOptions(HookExecutionMode.ExecuteOriginalInstructionFirst, HookRegister.RaxEax)); Assert.That(hookResult.IsSuccess, Is.True); ProceedUntilProcessEnds(); - // After execution, the long in the output at index 5 must reflect the new value written by the hook. + // After execution, all values should be the expected ones. AssertExpectedFinalResults(); } @@ -86,7 +87,7 @@ public void HookRevertTest() // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, - new HookOptions(HookExecutionMode.ReplaceOriginalInstruction, Register.RAX)); + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); hookResult.Value.Revert(); // Revert the hook to restore the original code. ProceedUntilProcessEnds(); From 0ee9593a1ac6c537afa506ef7169b745f0447d40 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sun, 16 Jun 2024 16:17:53 +0200 Subject: [PATCH 24/66] More Hook signatures and tests --- src/MindControl.Code/Hooks/HookOptions.cs | 3 + .../Hooks/ProcessMemory.Hooks.cs | 222 +++++++++++++++--- src/MindControl.Code/Results/HookFailure.cs | 15 ++ .../Allocation/MemoryReservation.cs | 16 +- .../ProcessMemory/ProcessMemory.Allocation.cs | 10 +- .../ProcessMemoryHookExtensionsTest.cs | 141 ++++++++++- 6 files changed, 368 insertions(+), 39 deletions(-) diff --git a/src/MindControl.Code/Hooks/HookOptions.cs b/src/MindControl.Code/Hooks/HookOptions.cs index 0f7cfe3..9aa8b17 100644 --- a/src/MindControl.Code/Hooks/HookOptions.cs +++ b/src/MindControl.Code/Hooks/HookOptions.cs @@ -34,12 +34,15 @@ public enum HookJumpMode /// Use a near jump if possible. If a near jump is not possible, fall back to a far jump. /// This is the safest option, as it will always work, but may not always give you the best performance (although /// it should in most cases). + /// For 32-bit processes, this mode is equivalent to , as near jumps are always possible. /// NearJumpWithFallbackOnFarJump, /// /// Use a near jump only. If a near jump is not possible, the hook operation will fail. /// Use this only if hook performance is critical and a far jump would not be acceptable. + /// For 32-bit processes, this mode is equivalent to , as near jumps are + /// always possible. /// NearJumpOnly } diff --git a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs index 73c5136..0a0b7ce 100644 --- a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs +++ b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs @@ -23,47 +23,185 @@ public static class ProcessMemoryHookExtensions /// Maximum byte count for a single instruction. private const int MaxInstructionLength = 15; + + #region Public hook methods + + /// + /// Injects code into the target process to be executed when the instruction at the executable address pointed by + /// the given pointer path is reached. Depending on the options, the injected code may replace the original target + /// instruction, or get executed either before or after it. If specified, additional instrutions that save and + /// restore registers will be added to the injected code. + /// Execution of the original code will then continue normally (unless the provided code is designed otherwise). + /// This signature uses a byte array containing the code to inject. If your code contains instructions with relative + /// operands (like jumps or calls), they may not point to the right addresses. In these cases, prefer the + /// signature. + /// + /// Process memory instance to use. + /// Pointer path to the first byte of the instruction to hook into. + /// + /// Assembled code to inject into the target process. The jump back to the original code will be + /// appended automatically, so it is not necessary to include it. Warning: if your code contains instructions with + /// relative operands (like jumps or calls), they may not point to the right addresses. In these cases, prefer the + /// signature. + /// Options defining how the hook works. + /// A result holding either a code hook instance that contains a reference to the injected code reservation + /// and allows you to revert the hook, or a hook failure when the operation failed. + public static Result Hook(this ProcessMemory processMemory, + PointerPath targetInstructionPointerPath, byte[] code, HookOptions options) + { + var addressResult = processMemory.EvaluateMemoryAddress(targetInstructionPointerPath); + if (addressResult.IsFailure) + return new HookFailureOnPathEvaluation(addressResult.Error); + + return processMemory.Hook(addressResult.Value, code, options); + } /// /// Injects code into the target process to be executed when the instruction at the given executable address is - /// reached. - /// Execution of the original code will continue normally (unless the given hook code is designed otherwise). - /// With default options, some code will be generated to save and restore state, so that the hook code is isolated - /// from the original code. + /// reached. Depending on the options, the injected code may replace the original target instruction, or get + /// executed either before or after it. If specified, additional instrutions that save and restore registers will be + /// added to the injected code. + /// Execution of the original code will then continue normally (unless the provided code is designed otherwise). + /// This signature uses a byte array containing the code to inject. If your code contains instructions with relative + /// operands (like jumps or calls), they may not point to the right addresses. In these cases, prefer the + /// signature. /// /// Process memory instance to use. /// Address of the first byte of the instruction to hook into. - /// Assembled code to inject into the target process. Warning: if your code contains - /// instructions with relative operands (like most jumps or calls), they will break. Use the - /// signature to make sure they still point to the - /// right address. + /// Assembled code to inject into the target process. The jump back to the original code will be + /// appended automatically, so it is not necessary to include it. Warning: if your code contains instructions with + /// relative operands (like jumps or calls), they may not point to the right addresses. In these cases, prefer the + /// signature. /// Options defining how the hook works. /// A result holding either a code hook instance that contains a reference to the injected code reservation /// and allows you to revert the hook, or a hook failure when the operation failed. public static Result Hook(this ProcessMemory processMemory, UIntPtr targetInstructionAddress, - byte[] injectedCode, HookOptions options) + byte[] code, HookOptions options) { + if (targetInstructionAddress == UIntPtr.Zero) + return new HookFailureOnZeroPointer(); + if (code.Length == 0) + return new HookFailureOnInvalidArguments("The code to inject must contain at least one byte."); + ulong sizeToReserve = (ulong)(options.GetExpectedGeneratedCodeSize(processMemory.Is64Bit) - + injectedCode.Length + + code.Length + MaxInstructionLength // Extra room for the original instructions + FarJumpInstructionLength); // Extra room for the jump back to the original code - // Reserve memory for the injected code. Try to reserve memory close enough to the target instruction to use a - // near jump, if possible. - var reservationResult = ReserveNearHookTarget(processMemory, sizeToReserve, targetInstructionAddress); - if (reservationResult.IsFailure && options.JumpMode == HookJumpMode.NearJumpOnly) - return new HookFailureOnAllocationFailure(reservationResult.Error); + // Reserve memory for the injected code as close as possible to the target instruction + var reservationResult = ReserveHookTarget(processMemory, sizeToReserve, targetInstructionAddress, + options.JumpMode); + if (reservationResult.IsFailure) + return reservationResult.Error; + var reservation = reservationResult.Value; + + return PerformHook(processMemory, targetInstructionAddress, code, reservation, options); + } + + /// + /// Injects code into the target process to be executed when the instruction at the executable address pointed by + /// the given pointer path is reached. Depending on the options, the injected code may replace the original target + /// instruction, or get executed either before or after it. If specified, additional instrutions that save and + /// restore registers will be added to the injected code. + /// Execution of the original code will then continue normally (unless the provided code is designed otherwise). + /// This signature uses an assembler, which is recommended, especially if your code contains instructions with + /// relative operands (like jumps or calls), because the assembler will adjust addresses to guarantee they point to + /// the right locations. + /// + /// Process memory instance to use. + /// Pointer path to the first byte of the instruction to hook into. + /// + /// Code assembler loaded with instructions to inject into the target process. The jump + /// back to the original code will be appended automatically, so it is not necessary to include it. + /// Options defining how the hook works. + /// A result holding either a code hook instance that contains a reference to the injected code reservation + /// and allows you to revert the hook, or a hook failure when the operation failed. + public static Result Hook(this ProcessMemory processMemory, + PointerPath targetInstructionPointerPath, Assembler codeAssembler, HookOptions options) + { + var addressResult = processMemory.EvaluateMemoryAddress(targetInstructionPointerPath); + if (addressResult.IsFailure) + return new HookFailureOnPathEvaluation(addressResult.Error); + return processMemory.Hook(addressResult.Value, codeAssembler, options); + } + + /// + /// Injects code into the target process to be executed when the instruction at the given executable address is + /// reached. Depending on the options, the injected code may replace the original target instruction, or get + /// executed either before or after it. If specified, additional instrutions that save and restore registers will be + /// added to the injected code. + /// Execution of the original code will then continue normally (unless the provided code is designed otherwise). + /// This signature uses an assembler, which is recommended, especially if your code contains instructions with + /// relative operands (like jumps or calls), because the assembler will adjust addresses to guarantee they point to + /// the right locations. + /// + /// Process memory instance to use. + /// Address of the first byte of the instruction to hook into. + /// Code assembler loaded with instructions to inject into the target process. The jump + /// back to the original code will be appended automatically, so it is not necessary to include it. + /// Options defining how the hook works. + /// A result holding either a code hook instance that contains a reference to the injected code reservation + /// and allows you to revert the hook, or a hook failure when the operation failed. + public static Result Hook(this ProcessMemory processMemory, UIntPtr targetInstructionAddress, + Assembler codeAssembler, HookOptions options) + { + if (targetInstructionAddress == UIntPtr.Zero) + return new HookFailureOnZeroPointer(); + if (codeAssembler.Instructions.Count == 0) + return new HookFailureOnInvalidArguments("The given code assembler must have at least one instruction."); + + // The problem with using an assembler is that we don't know how many bytes the assembled code will take, so we + // have to use the most conservative estimate possible, which is the maximum length of an instruction multiplied + // by the number of instructions in the assembler. + int codeMaxLength = MaxInstructionLength * codeAssembler.Instructions.Count; + ulong sizeToReserve = (ulong)(options.GetExpectedGeneratedCodeSize(processMemory.Is64Bit) + + codeMaxLength + + MaxInstructionLength // Extra room for the original instructions + + FarJumpInstructionLength); // Extra room for the jump back to the original code + + // Reserve memory for the injected code, as close as possible to the target instruction + var reservationResult = ReserveHookTarget(processMemory, sizeToReserve, targetInstructionAddress, + options.JumpMode); if (reservationResult.IsFailure) + return reservationResult.Error; + var reservation = reservationResult.Value; + + // Assemble the code to inject + var assemblingResult = codeAssembler.AssembleToBytes(reservation.Address, 128); + if (assemblingResult.IsFailure) { - // If we cannot reserve memory for a near jump, try going for a far jump - reservationResult = processMemory.Reserve(sizeToReserve, true); - if (reservationResult.IsFailure) - return new HookFailureOnAllocationFailure(reservationResult.Error); + reservation.Dispose(); + return new HookFailureOnCodeAssembly(HookCodeAssemblySource.InjectedCode, assemblingResult.Error); } + byte[] codeBytes = assemblingResult.Value; + + // Resize the reservation now that we have more accurate information + int difference = codeMaxLength - codeBytes.Length; + if (difference > 0) + reservation.Shrink((ulong)difference); + return PerformHook(processMemory, targetInstructionAddress, codeBytes, reservation, options); + } + + #endregion + + #region Internal hook methods + + /// + /// Method called internally by the hook methods to assemble the full code to inject, write it in the given + /// reserved memory, and write the jump to the injected code at the target instruction address. + /// + /// Process memory instance to use. + /// Address of the first byte of the instruction to hook into. + /// Assembled code to inject into the target process. + /// Memory reservation where the injected code will be written. + /// Options defining how the hook behaves. + /// A result holding either a code hook instance, or a hook failure when the operation failed. + private static Result PerformHook(ProcessMemory processMemory, + UIntPtr targetInstructionAddress, byte[] code, MemoryReservation reservation, HookOptions options) + { // Assemble the jump to the injected code - var reservation = reservationResult.Value; var jmpAssembler = new Assembler(processMemory.Is64Bit ? 64 : 32); jmpAssembler.jmp(reservation.Address); var jmpAssembleResult = jmpAssembler.AssembleToBytes(targetInstructionAddress); @@ -123,7 +261,7 @@ public static Result Hook(this ProcessMemory processMemor // Assemble the post-code var postHookCodeAddress = (UIntPtr)(reservation.Address + (ulong)preHookCodeBytes.Length - + (ulong)injectedCode.Length); + + (ulong)code.Length); var postHookCodeResult = BuildPostHookCode(postHookCodeAddress, instructionsToReplace, processMemory.Is64Bit, options, nextOriginalInstructionAddress); if (postHookCodeResult.IsFailure) @@ -134,7 +272,7 @@ public static Result Hook(this ProcessMemory processMemor byte[] postHookCodeBytes = postHookCodeResult.Value; // Assemble the full code to inject - var fullCodeLength = preHookCodeBytes.Length + injectedCode.Length + postHookCodeBytes.Length; + var fullCodeLength = preHookCodeBytes.Length + code.Length + postHookCodeBytes.Length; if ((ulong)fullCodeLength > reservation.Size) { reservation.Dispose(); @@ -143,8 +281,8 @@ public static Result Hook(this ProcessMemory processMemor } var fullInjectedCode = new byte[fullCodeLength]; Buffer.BlockCopy(preHookCodeBytes, 0, fullInjectedCode, 0, preHookCodeBytes.Length); - Buffer.BlockCopy(injectedCode, 0, fullInjectedCode, preHookCodeBytes.Length, injectedCode.Length); - Buffer.BlockCopy(postHookCodeBytes, 0, fullInjectedCode, preHookCodeBytes.Length + injectedCode.Length, + Buffer.BlockCopy(code, 0, fullInjectedCode, preHookCodeBytes.Length, code.Length); + Buffer.BlockCopy(postHookCodeBytes, 0, fullInjectedCode, preHookCodeBytes.Length + code.Length, postHookCodeBytes.Length); // Write the assembled code to the reserved memory @@ -165,7 +303,7 @@ public static Result Hook(this ProcessMemory processMemor return new CodeHook(processMemory, targetInstructionAddress, originalBytesResult.Value, reservation); } - + /// /// Assembles the code that comes before the user-made injected code in a hook operation. /// @@ -271,14 +409,15 @@ private static Result BuildPostHookCode(ulong baseAddress, } /// - /// Attempts to reserve executable memory close to the hook target instruction. + /// Attempts to reserve executable memory as close as possible to the hook target instruction. /// /// Target process memory instance. /// Size of the memory to reserve. /// Address of the jump instruction that will jump to the injected code. + /// Jump mode of the hook. /// A result holding either the memory reservation, or an allocation failure. - private static Result ReserveNearHookTarget(ProcessMemory processMemory, - ulong sizeToReserve, UIntPtr jumpAddress) + private static Result ReserveHookTarget(ProcessMemory processMemory, + ulong sizeToReserve, UIntPtr jumpAddress, HookJumpMode jumpMode) { // The range of a near jump is limited by the signed byte displacement that follows the opcode. // The displacement is a signed 4-byte integer, so the range is from -2GB to +2GB. @@ -287,8 +426,13 @@ private static Result ReserveNearHookTarge // For example, a jump from 0x00000000 to 0xFFFFFFFF can be made with a near jump with an offset of -1. // Which means that for 32-bit processes, we can reserve memory anywhere in the address space. if (!processMemory.Is64Bit) - return processMemory.Reserve(sizeToReserve, true, nearAddress: jumpAddress); - + { + var reservationResult = processMemory.Reserve(sizeToReserve, true, nearAddress: jumpAddress); + if (reservationResult.IsFailure) + return new HookFailureOnAllocationFailure(reservationResult.Error); + return reservationResult.Value; + } + // In 64-bit processes, however, the offset is still a signed 4-byte integer, but the full range is much // larger, meaning we can't reach any address. @@ -305,10 +449,24 @@ private static Result ReserveNearHookTarge var extraCloseRange = nextInstructionAddress.GetRangeAround(int.MaxValue); var extraCloseReservation = processMemory.Reserve(sizeToReserve, true, extraCloseRange, nextInstructionAddress); if (extraCloseReservation.IsSuccess) - return extraCloseReservation; + return extraCloseReservation.Value; // If the extra close reservation failed, use the full range of a near jump var nearJumpRange = nextInstructionAddress.GetRangeAround(uint.MaxValue); - return processMemory.Reserve(sizeToReserve, true, nearJumpRange, nextInstructionAddress); + var nearJumpResult = processMemory.Reserve(sizeToReserve, true, nearJumpRange, nextInstructionAddress); + if (nearJumpResult.IsSuccess) + return nearJumpResult.Value; + + // If the jump mode is set to near jump only, we return the failure, as we would not be able to use a near jump. + if (jumpMode == HookJumpMode.NearJumpOnly) + return new HookFailureOnAllocationFailure(nearJumpResult.Error); + + // Otherwise, we try to reserve memory anywhere in the address space + var fullRangeResult = processMemory.Reserve(sizeToReserve, true, nearAddress: jumpAddress); + if (fullRangeResult.IsFailure) + return new HookFailureOnAllocationFailure(fullRangeResult.Error); + return fullRangeResult.Value; } + + #endregion } \ No newline at end of file diff --git a/src/MindControl.Code/Results/HookFailure.cs b/src/MindControl.Code/Results/HookFailure.cs index fd8ad31..8d1cbb2 100644 --- a/src/MindControl.Code/Results/HookFailure.cs +++ b/src/MindControl.Code/Results/HookFailure.cs @@ -12,6 +12,11 @@ public enum HookFailureReason /// PathEvaluationFailure, + /// + /// The target address is a zero pointer. + /// + ZeroPointer, + /// /// The arguments provided to the hook operation are invalid. /// @@ -61,6 +66,16 @@ public record HookFailureOnPathEvaluation(PathEvaluationFailure Details) public override string ToString() => $"Failed to evaluate the given pointer path: {Details}"; } +/// +/// Represents a failure that occurred in a hook operation when the target address is a zero pointer. +/// +public record HookFailureOnZeroPointer() : HookFailure(HookFailureReason.ZeroPointer) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => "The target address is a zero pointer."; +} + /// /// Represents a failure that occurred in a hook operation when the arguments provided are invalid. /// diff --git a/src/MindControl/Allocation/MemoryReservation.cs b/src/MindControl/Allocation/MemoryReservation.cs index 4a049a4..aa19e76 100644 --- a/src/MindControl/Allocation/MemoryReservation.cs +++ b/src/MindControl/Allocation/MemoryReservation.cs @@ -11,7 +11,7 @@ public class MemoryReservation /// /// Gets the memory range of this reservation. /// - public MemoryRange Range { get; } + public MemoryRange Range { get; private set; } /// /// Gets the starting address of the reservation. @@ -56,4 +56,18 @@ public void Dispose() IsDisposed = true; ParentAllocation.FreeReservation(this); } + + /// + /// Shrinks the reservation by the given number of bytes. + /// + /// Number of bytes to remove from the reservation. + /// Thrown when the difference is equal to or greater than the size of the + /// reservation. + public void Shrink(ulong difference) + { + if (difference >= Size) + throw new ArgumentException("Cannot shrink the reservation by its full size or more."); + + Range = new MemoryRange(Range.Start, unchecked((UIntPtr)(Range.End.ToUInt64() - difference))); + } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index 63701c9..9e489ca 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -95,8 +95,8 @@ private Result FindAndAllocateFreeMemory(ulong s MemoryRange? freeRange = null; MemoryRangeMetadata currentMetadata; - while ((nextAddressForward.ToUInt64() < actualRange.Value.End.ToUInt64() - || nextAddressBackwards.ToUInt64() > actualRange.Value.Start.ToUInt64()) + while ((nextAddress.ToUInt64() <= actualRange.Value.End.ToUInt64() + && nextAddress.ToUInt64() >= actualRange.Value.Start.ToUInt64()) && (currentMetadata = _osService.GetRegionMetadata(ProcessHandle, nextAddress, Is64Bit) .GetValueOrDefault()).Size.ToUInt64() > 0) { @@ -117,7 +117,8 @@ private Result FindAndAllocateFreeMemory(ulong s { var forwardDistance = nearAddress.Value.DistanceTo(nextAddressForward); var backwardDistance = nearAddress.Value.DistanceTo(nextAddressBackwards); - goingForward = forwardDistance <= backwardDistance; + goingForward = forwardDistance <= backwardDistance + && nextAddressForward.ToUInt64() <= actualRange.Value.End.ToUInt64(); nextAddress = goingForward ? nextAddressForward : nextAddressBackwards; } @@ -164,7 +165,8 @@ private Result FindAndAllocateFreeMemory(ulong s { var forwardDistance = nearAddress.Value.DistanceTo(nextAddressForward); var backwardDistance = nearAddress.Value.DistanceTo(nextAddressBackwards); - goingForward = forwardDistance <= backwardDistance; + goingForward = forwardDistance <= backwardDistance + && nextAddressForward.ToUInt64() <= actualRange.Value.End.ToUInt64(); nextAddress = goingForward ? nextAddressForward : nextAddressBackwards; } diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs index 4f53638..bf7a7bb 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs @@ -1,5 +1,6 @@ using Iced.Intel; using MindControl.Hooks; +using MindControl.Results; using NUnit.Framework; namespace MindControl.Test.ProcessMemoryTests.CodeExtensions; @@ -18,7 +19,7 @@ public class ProcessMemoryHookExtensionsTest : ProcessMemoryTest /// After hooking, we let the program run, and check that the output long value is the one written by the hook. /// [Test] - public void HookAndReplaceValueOfMovInstructionTest() + public void HookAndReplaceMovWithByteArrayTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); @@ -50,7 +51,7 @@ public void HookAndReplaceValueOfMovInstructionTest() /// because we specified that the RAX register should be isolated. /// [Test] - public void HookAndInsertValueOfMovInstructionWithRegisterIsolationTest() + public void HookAndInsertMovWithRegisterIsolationWithByteArrayTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); @@ -70,6 +71,142 @@ public void HookAndInsertValueOfMovInstructionWithRegisterIsolationTest() AssertExpectedFinalResults(); } + /// + /// Tests the method. + /// The hook replaces a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the + /// long value in the target app, with a new MOV instruction that assigns a different value. + /// No registers are preserved. + /// After hooking, we let the program run, and check that the output long value is the one written by the hook. + /// + [Test] + public void HookAndReplaceMovWithAssemblerTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + + // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. + var hookResult = TestProcessMemory!.Hook(movLongAddress, assembler, + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); + + Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.Value.InjectedCodeReservation, Is.Not.Null); + Assert.That(hookResult.Value.Address, Is.EqualTo(movLongAddress)); + Assert.That(hookResult.Value.Length, Is.AtLeast(5)); + + ProceedUntilProcessEnds(); + + // After execution, the long in the output at index 5 must reflect the new value written by the hook. + AssertFinalResults(5, "1234567890"); + } + + /// + /// Tests the method. + /// The hook targets a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the + /// long value in the target app, and inserts a new MOV instruction that assigns a different value after the + /// instruction. + /// However, we specify that the should be preserved. + /// After hooking, we let the program run, and check the output. The long value must be the expected, original one, + /// because we specified that the RAX register should be isolated. + /// + [Test] + public void HookAndInsertMovWithRegisterIsolationWithAssemblerTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + + var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + + // Hook the instruction that writes the long value to RAX, to append code that writes another value. + // Specify that the RAX register should be isolated. + var hookResult = TestProcessMemory!.Hook(movLongAddress, assembler, + new HookOptions(HookExecutionMode.ExecuteOriginalInstructionFirst, HookRegister.RaxEax)); + Assert.That(hookResult.IsSuccess, Is.True); + + ProceedUntilProcessEnds(); + + // After execution, all values should be the expected ones. + AssertExpectedFinalResults(); + } + + /// + /// Tests the method. + /// The method is called with a zero pointer address. + /// Expects a result. + /// + [Test] + public void HookWithZeroAddressTest() + { + var hookResult = TestProcessMemory!.Hook(UIntPtr.Zero, new byte[5], + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); + Assert.That(hookResult.IsFailure, Is.True); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests the method. + /// The method is called with a pointer path that does not evaluate to a valid address. + /// Expects a result. + /// + [Test] + public void HookWithBadPathWithByteArrayTest() + { + var hookResult = TestProcessMemory!.Hook(new PointerPath("bad pointer path"), new byte[5], + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); + Assert.That(hookResult.IsFailure, Is.True); + Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(((HookFailureOnPathEvaluation)hookResult.Error).Details, Is.Not.Null); + } + + /// + /// Tests the + /// method. + /// The method is called with a pointer path that does not evaluate to a valid address. + /// Expects a result. + /// + [Test] + public void HookWithBadPathWithAssemblerTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var hookResult = TestProcessMemory!.Hook(new PointerPath("bad pointer path"), assembler, + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); + Assert.That(hookResult.IsFailure, Is.True); + Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(((HookFailureOnPathEvaluation)hookResult.Error).Details, Is.Not.Null); + } + + /// + /// Tests the method. + /// The hook is called with an empty code byte array. + /// Expects a result. + /// + [Test] + public void HookWithEmptyCodeArrayTest() + { + var address = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + var hookResult = TestProcessMemory!.Hook(address, [], + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); + Assert.That(hookResult.IsFailure, Is.True); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests the method. + /// The hook is called with an assembler that does not have any instructions. + /// Expects a result. + /// + [Test] + public void HookWithEmptyAssemblerTest() + { + var address = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + var assembler = new Assembler(64); + var hookResult = TestProcessMemory!.Hook(address, assembler, + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); + Assert.That(hookResult.IsFailure, Is.True); + Assert.That(hookResult.Error, Is.TypeOf()); + } + /// /// Tests the method. /// The hook replaces a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the From ad55ed97cf4c7446d67657f5d71e02e997e75ca2 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Mon, 17 Jun 2024 21:17:07 +0200 Subject: [PATCH 25/66] New InsertCodeAt hook method --- .../Hooks/ProcessMemory.Hooks.cs | 132 ++++++++++++-- .../ProcessMemoryHookExtensionsTest.cs | 167 +++++++++++++++++- 2 files changed, 282 insertions(+), 17 deletions(-) diff --git a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs index 0a0b7ce..ccc3b70 100644 --- a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs +++ b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs @@ -29,11 +29,11 @@ public static class ProcessMemoryHookExtensions /// /// Injects code into the target process to be executed when the instruction at the executable address pointed by /// the given pointer path is reached. Depending on the options, the injected code may replace the original target - /// instruction, or get executed either before or after it. If specified, additional instrutions that save and + /// instruction, or get executed either before or after it. If specified, additional instructions that save and /// restore registers will be added to the injected code. /// Execution of the original code will then continue normally (unless the provided code is designed otherwise). /// This signature uses a byte array containing the code to inject. If your code contains instructions with relative - /// operands (like jumps or calls), they may not point to the right addresses. In these cases, prefer the + /// operands (like jumps or calls), they may not point to the intended address. In these cases, prefer the /// signature. /// /// Process memory instance to use. @@ -41,7 +41,7 @@ public static class ProcessMemoryHookExtensions /// /// Assembled code to inject into the target process. The jump back to the original code will be /// appended automatically, so it is not necessary to include it. Warning: if your code contains instructions with - /// relative operands (like jumps or calls), they may not point to the right addresses. In these cases, prefer the + /// relative operands (like jumps or calls), they may not point to the intended address. In these cases, prefer the /// signature. /// Options defining how the hook works. /// A result holding either a code hook instance that contains a reference to the injected code reservation @@ -59,18 +59,18 @@ public static Result Hook(this ProcessMemory processMemor /// /// Injects code into the target process to be executed when the instruction at the given executable address is /// reached. Depending on the options, the injected code may replace the original target instruction, or get - /// executed either before or after it. If specified, additional instrutions that save and restore registers will be + /// executed either before or after it. If specified, additional instructions that save and restore registers will be /// added to the injected code. /// Execution of the original code will then continue normally (unless the provided code is designed otherwise). /// This signature uses a byte array containing the code to inject. If your code contains instructions with relative - /// operands (like jumps or calls), they may not point to the right addresses. In these cases, prefer the + /// operands (like jumps or calls), they may not point to the intended address. In these cases, prefer the /// signature. /// /// Process memory instance to use. /// Address of the first byte of the instruction to hook into. /// Assembled code to inject into the target process. The jump back to the original code will be /// appended automatically, so it is not necessary to include it. Warning: if your code contains instructions with - /// relative operands (like jumps or calls), they may not point to the right addresses. In these cases, prefer the + /// relative operands (like jumps or calls), they may not point to the intended address. In these cases, prefer the /// signature. /// Options defining how the hook works. /// A result holding either a code hook instance that contains a reference to the injected code reservation @@ -101,12 +101,12 @@ public static Result Hook(this ProcessMemory processMemor /// /// Injects code into the target process to be executed when the instruction at the executable address pointed by /// the given pointer path is reached. Depending on the options, the injected code may replace the original target - /// instruction, or get executed either before or after it. If specified, additional instrutions that save and + /// instruction, or get executed either before or after it. If specified, additional instructions that save and /// restore registers will be added to the injected code. /// Execution of the original code will then continue normally (unless the provided code is designed otherwise). /// This signature uses an assembler, which is recommended, especially if your code contains instructions with /// relative operands (like jumps or calls), because the assembler will adjust addresses to guarantee they point to - /// the right locations. + /// the intended locations. /// /// Process memory instance to use. /// Pointer path to the first byte of the instruction to hook into. @@ -129,12 +129,12 @@ public static Result Hook(this ProcessMemory processMemor /// /// Injects code into the target process to be executed when the instruction at the given executable address is /// reached. Depending on the options, the injected code may replace the original target instruction, or get - /// executed either before or after it. If specified, additional instrutions that save and restore registers will be + /// executed either before or after it. If specified, additional instructions that save and restore registers will be /// added to the injected code. /// Execution of the original code will then continue normally (unless the provided code is designed otherwise). /// This signature uses an assembler, which is recommended, especially if your code contains instructions with /// relative operands (like jumps or calls), because the assembler will adjust addresses to guarantee they point to - /// the right locations. + /// the intended locations. /// /// Process memory instance to use. /// Address of the first byte of the instruction to hook into. @@ -184,6 +184,118 @@ public static Result Hook(this ProcessMemory processMemor return PerformHook(processMemory, targetInstructionAddress, codeBytes, reservation, options); } + /// + /// Injects code in the process to be executed right before the instruction pointed by the given pointer path, by + /// performing a hook. + /// This signature uses a byte array containing the code to inject. If your code contains instructions with relative + /// operands (like jumps or calls), they may not point to the intended address. In these cases, prefer the + /// signature. + /// + /// + /// This method is essentially a shortcut for with + /// the execution mode set to . It is provided for + /// convenience, readability, and discoverability for users who might not be familiar with hooks but are looking to + /// achieve the same result. + /// + /// Process memory instance to use. + /// Pointer path to the first byte of the target instruction. The + /// injected code will be executed just before the instruction. + /// Assembled code to inject into the target process. + /// Optionally provided registers to be saved and restored around the injected + /// code. This allows the injected code to modify registers without affecting the original code, which could + /// otherwise lead to crashes or unexpected behavior. + /// A result holding either a code hook instance that contains the memory reservation holding the + /// injected code and also allows you to revert the operation, or a hook failure when the operation failed. + /// + public static Result InsertCodeAt(this ProcessMemory processMemory, + PointerPath targetInstructionPointerPath, byte[] code, params HookRegister[] registersToPreserve) + => processMemory.Hook(targetInstructionPointerPath, code, + new HookOptions(HookExecutionMode.ExecuteInjectedCodeFirst, registersToPreserve)); + + /// + /// Injects code in the process to be executed right before the instruction at the given address, by performing a + /// hook. + /// This signature uses a byte array containing the code to inject. If your code contains instructions with relative + /// operands (like jumps or calls), they may not point to the intended address. In these cases, prefer the + /// signature. + /// + /// + /// This method is essentially a shortcut for with the + /// execution mode set to . It is provided for convenience, + /// readability, and discoverability for users who might not be familiar with hooks but are looking to achieve the + /// same result. + /// + /// Process memory instance to use. + /// Address of the first byte of the target instruction. The injected code + /// will be executed just before the instruction. + /// Assembled code to inject into the target process. + /// Optionally provided registers to be saved and restored around the injected + /// code. This allows the injected code to modify registers without affecting the original code, which could + /// otherwise lead to crashes or unexpected behavior. + /// A result holding either a code hook instance that contains the memory reservation holding the + /// injected code and also allows you to revert the operation, or a hook failure when the operation failed. + /// + public static Result InsertCodeAt(this ProcessMemory processMemory, + UIntPtr targetInstructionAddress, byte[] code, params HookRegister[] registersToPreserve) + => processMemory.Hook(targetInstructionAddress, code, + new HookOptions(HookExecutionMode.ExecuteInjectedCodeFirst, registersToPreserve)); + + /// + /// Injects code in the process to be executed right before the instruction pointed by the given pointer path, by + /// performing a hook. + /// This signature uses an assembler, which is recommended, especially if your code contains instructions with + /// relative operands (like jumps or calls), because the assembler will adjust addresses to guarantee they point to + /// the intended locations. + /// + /// + /// This method is essentially a shortcut for + /// with the execution mode set to . It is provided for + /// convenience, readability, and discoverability for users who might not be familiar with hooks but are looking to + /// achieve the same result. + /// + /// Process memory instance to use. + /// Pointer path to the first byte of the target instruction. The + /// injected code will be executed just before the instruction. + /// Code assembler loaded with instructions to inject into the target process. + /// Optionally provided registers to be saved and restored around the injected + /// code. This allows the injected code to modify registers without affecting the original code, which could + /// otherwise lead to crashes or unexpected behavior. + /// A result holding either a code hook instance that contains the memory reservation holding the + /// injected code and also allows you to revert the operation, or a hook failure when the operation failed. + /// + public static Result InsertCodeAt(this ProcessMemory processMemory, + PointerPath targetInstructionPointerPath, Assembler codeAssembler, params HookRegister[] registersToPreserve) + => processMemory.Hook(targetInstructionPointerPath, codeAssembler, + new HookOptions(HookExecutionMode.ExecuteInjectedCodeFirst, registersToPreserve)); + + /// + /// Injects code in the process to be executed right before the instruction at the given address, by performing a + /// hook. + /// This signature uses an assembler, which is recommended, especially if your code contains instructions with + /// relative operands (like jumps or calls), because the assembler will adjust addresses to guarantee they point to + /// the intended locations. + /// + /// + /// This method is essentially a shortcut for with + /// the execution mode set to . It is provided for + /// convenience, readability, and discoverability for users who might not be familiar with hooks but are looking to + /// achieve the same result. + /// + /// Process memory instance to use. + /// Address of the first byte of the target instruction. The injected code + /// will be executed just before the instruction. + /// Code assembler loaded with instructions to inject into the target process. + /// Optionally provided registers to be saved and restored around the injected + /// code. This allows the injected code to modify registers without affecting the original code, which could + /// otherwise lead to crashes or unexpected behavior. + /// A result holding either a code hook instance that contains the memory reservation holding the + /// injected code and also allows you to revert the operation, or a hook failure when the operation failed. + /// + public static Result InsertCodeAt(this ProcessMemory processMemory, + UIntPtr targetInstructionAddress, Assembler codeAssembler, params HookRegister[] registersToPreserve) + => processMemory.Hook(targetInstructionAddress, codeAssembler, + new HookOptions(HookExecutionMode.ExecuteInjectedCodeFirst, registersToPreserve)); + #endregion #region Internal hook methods diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs index bf7a7bb..ae27de5 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs @@ -11,6 +11,10 @@ namespace MindControl.Test.ProcessMemoryTests.CodeExtensions; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ProcessMemoryHookExtensionsTest : ProcessMemoryTest { + private const string MovLongValueInstructionBytePattern = "48 B8 DF 54 09 2B BA 3C FD FF"; + + #region Hook + /// /// Tests the method. /// The hook replaces a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the @@ -24,7 +28,7 @@ public void HookAndReplaceMovWithByteArrayTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, @@ -57,7 +61,7 @@ public void HookAndInsertMovWithRegisterIsolationWithByteArrayTest() assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); // Hook the instruction that writes the long value to RAX, to append code that writes another value. // Specify that the RAX register should be isolated. @@ -83,7 +87,7 @@ public void HookAndReplaceMovWithAssemblerTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. var hookResult = TestProcessMemory!.Hook(movLongAddress, assembler, @@ -115,7 +119,7 @@ public void HookAndInsertMovWithRegisterIsolationWithAssemblerTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); // Hook the instruction that writes the long value to RAX, to append code that writes another value. // Specify that the RAX register should be isolated. @@ -184,7 +188,7 @@ public void HookWithBadPathWithAssemblerTest() [Test] public void HookWithEmptyCodeArrayTest() { - var address = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + var address = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); var hookResult = TestProcessMemory!.Hook(address, [], new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsFailure, Is.True); @@ -199,7 +203,7 @@ public void HookWithEmptyCodeArrayTest() [Test] public void HookWithEmptyAssemblerTest() { - var address = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + var address = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); var assembler = new Assembler(64); var hookResult = TestProcessMemory!.Hook(address, assembler, new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); @@ -220,7 +224,7 @@ public void HookRevertTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, @@ -230,4 +234,153 @@ public void HookRevertTest() ProceedUntilProcessEnds(); AssertFinalResults(5, ExpectedFinalValues[5]); } + + #endregion + + #region InsertCodeAt + + /// + /// Tests the + /// method. + /// Inserts a new MOV instruction that assigns a different value after the instruction that writes a long value to + /// the RAX register. This should change the output long value. + /// + [Test] + public void InsertCodeAtWithByteArrayTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + var movLongNextInstructionAddress = + TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First() + 10; + + // Insert the code right after our target MOV instruction. + // That way, the RAX register will be set to the value we want before it's used to write the new long value. + var hookResult = TestProcessMemory!.InsertCodeAt(movLongNextInstructionAddress, bytes); + Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.Value.InjectedCodeReservation, Is.Not.Null); + Assert.That(hookResult.Value.Address, Is.EqualTo(movLongNextInstructionAddress)); + Assert.That(hookResult.Value.Length, Is.AtLeast(5)); + + ProceedUntilProcessEnds(); + + // After execution, the long in the output at index 5 must reflect the new value written by the hook. + AssertFinalResults(5, "1234567890"); + } + + /// + /// Tests the + /// method. + /// Inserts a new MOV instruction that assigns a different value after the instruction that writes a long value to + /// the RAX register. However, we specify that RAX should be preserved. + /// The output long value must be the original one, because the RAX register is isolated. + /// + [Test] + public void InsertCodeAtWithByteArrayWithIsolationTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + var movLongNextInstructionAddress = + TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First() + 10; + + // Insert the code right after our target MOV instruction. + // That way, the RAX register will be set to the value we want before it's used to write the new long value. + // But because we specify that the RAX register should be preserved, after the hook, the RAX register will + // be restored to its original value. + var hookResult = TestProcessMemory!.InsertCodeAt(movLongNextInstructionAddress, bytes, HookRegister.RaxEax); + + Assert.That(hookResult.IsSuccess, Is.True); + ProceedUntilProcessEnds(); + AssertExpectedFinalResults(); + } + + /// + /// Tests the + /// method. + /// Inserts a new MOV instruction that assigns a different value after the instruction that writes a long value to + /// the RAX register. This should change the output long value. + /// + [Test] + public void InsertCodeAtWithAssemblerTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var movLongNextInstructionAddress = + TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First() + 10; + var hookResult = TestProcessMemory!.InsertCodeAt(movLongNextInstructionAddress, assembler); + + Assert.That(hookResult.IsSuccess, Is.True); + ProceedUntilProcessEnds(); + AssertFinalResults(5, "1234567890"); + } + + /// + /// Tests the + /// method. + /// Inserts a new MOV instruction that assigns a different value after the instruction that writes a long value to + /// the RAX register. However, we specify that RAX should be preserved. + /// The output long value must be the original one, because the RAX register is isolated. + /// + [Test] + public void InsertCodeAtWithAssemblerWithIsolationTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var movLongNextInstructionAddress = + TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First() + 10; + var hookResult = TestProcessMemory!.InsertCodeAt(movLongNextInstructionAddress, assembler, HookRegister.RaxEax); + + Assert.That(hookResult.IsSuccess, Is.True); + ProceedUntilProcessEnds(); + AssertExpectedFinalResults(); + } + + /// + /// Tests the + /// method. + /// Inserts a new MOV instruction that assigns a different value after the instruction that writes a long value to + /// the RAX register. This should change the output long value. + /// + [Test] + public void InsertCodeAtWithByteArrayWithPointerPathTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + var movLongNextInstructionAddress = + TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First() + 10; + var pointerPath = movLongNextInstructionAddress.ToString("X"); + + var hookResult = TestProcessMemory!.InsertCodeAt(pointerPath, bytes); + Assert.That(hookResult.IsSuccess, Is.True); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "1234567890"); + } + + /// + /// Tests the + /// + /// method. + /// Inserts a new MOV instruction that assigns a different value after the instruction that writes a long value to + /// the RAX register. This should change the output long value. + /// + [Test] + public void InsertCodeAtWithAssemblerWithPointerPathTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var movLongNextInstructionAddress = + TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First() + 10; + var pointerPath = movLongNextInstructionAddress.ToString("X"); + + var hookResult = TestProcessMemory!.InsertCodeAt(pointerPath, assembler); + Assert.That(hookResult.IsSuccess, Is.True); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "1234567890"); + } + + #endregion } \ No newline at end of file From 85b57bc356ef8650b6d3c87457547af4a27a2144 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Wed, 19 Jun 2024 22:36:10 +0200 Subject: [PATCH 26/66] New method: ReplaceCodeAt --- src/MindControl.Code/Hooks/HookOptions.cs | 48 ++- .../Hooks/ProcessMemory.Hooks.cs | 393 +++++++++++++++--- src/MindControl.Code/Results/HookFailure.cs | 16 +- .../ProcessMemoryHookExtensionsTest.cs | 385 +++++++++++++++++ 4 files changed, 779 insertions(+), 63 deletions(-) diff --git a/src/MindControl.Code/Hooks/HookOptions.cs b/src/MindControl.Code/Hooks/HookOptions.cs index 9aa8b17..da2139e 100644 --- a/src/MindControl.Code/Hooks/HookOptions.cs +++ b/src/MindControl.Code/Hooks/HookOptions.cs @@ -103,26 +103,52 @@ public HookOptions(HookExecutionMode executionMode, HookJumpMode jumpMode, } /// - /// Gets the predicted length in bytes of the additional code that will be prepended and appended to the hook code. + /// Gets the sum of and . /// /// Indicates if the target process is 64-bit. - /// The total size in bytes of the additional code. + /// The total size in bytes of the additional code that will be prepended and appended to the injected + /// code. internal int GetExpectedGeneratedCodeSize(bool is64Bit) + => GetExpectedPreCodeSize(is64Bit) + GetExpectedPostCodeSize(is64Bit); + + /// + /// Gets the predicted length in bytes of the additional code that will be prepended to the hook code. + /// + /// Indicates if the target process is 64-bit. + /// The size in bytes of the prepended code. + internal int GetExpectedPreCodeSize(bool is64Bit) + { + int totalSize = 0; + if (RegistersToPreserve.Contains(HookRegister.Flags)) + totalSize += AssemblerExtensions.GetSizeOfFlagsSave(is64Bit); + + totalSize += RegistersToPreserve.Select(r => r.ToRegister(is64Bit)) + .Where(r => r != null) + .Sum(register => AssemblerExtensions.GetSizeOfSaveInstructions(register!.Value, is64Bit)); + + if (RegistersToPreserve.Contains(HookRegister.FpuStack)) + totalSize += AssemblerExtensions.GetSizeOfFpuStackSave(is64Bit); + + return totalSize; + } + + /// + /// Gets the predicted length in bytes of the additional code that will be appended to the hook code. + /// + /// Indicates if the target process is 64-bit. + /// The size in bytes of the appended code. + internal int GetExpectedPostCodeSize(bool is64Bit) { int totalSize = 0; if (RegistersToPreserve.Contains(HookRegister.Flags)) - totalSize += AssemblerExtensions.GetSizeOfFlagsSave(is64Bit) - + AssemblerExtensions.GetSizeOfFlagsRestore(is64Bit); + totalSize += AssemblerExtensions.GetSizeOfFlagsRestore(is64Bit); - foreach (var register in RegistersToPreserve.Select(r => r.ToRegister(is64Bit)).Where(r => r != null)) - { - totalSize += AssemblerExtensions.GetSizeOfSaveInstructions(register!.Value, is64Bit); - totalSize += AssemblerExtensions.GetSizeOfRestoreInstructions(register.Value, is64Bit); - } + totalSize += RegistersToPreserve.Select(r => r.ToRegister(is64Bit)) + .Where(r => r != null) + .Sum(register => AssemblerExtensions.GetSizeOfRestoreInstructions(register!.Value, is64Bit)); if (RegistersToPreserve.Contains(HookRegister.FpuStack)) - totalSize += AssemblerExtensions.GetSizeOfFpuStackSave(is64Bit) - + AssemblerExtensions.GetSizeOfFpuStackRestore(is64Bit); + totalSize += AssemblerExtensions.GetSizeOfFpuStackRestore(is64Bit); return totalSize; } diff --git a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs index ccc3b70..2fd0524 100644 --- a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs +++ b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs @@ -82,13 +82,9 @@ public static Result Hook(this ProcessMemory processMemor return new HookFailureOnZeroPointer(); if (code.Length == 0) return new HookFailureOnInvalidArguments("The code to inject must contain at least one byte."); - - ulong sizeToReserve = (ulong)(options.GetExpectedGeneratedCodeSize(processMemory.Is64Bit) - + code.Length - + MaxInstructionLength // Extra room for the original instructions - + FarJumpInstructionLength); // Extra room for the jump back to the original code // Reserve memory for the injected code as close as possible to the target instruction + ulong sizeToReserve = GetSizeToReserveForCodeInjection(processMemory, code.Length, options); var reservationResult = ReserveHookTarget(processMemory, sizeToReserve, targetInstructionAddress, options.JumpMode); if (reservationResult.IsFailure) @@ -155,10 +151,7 @@ public static Result Hook(this ProcessMemory processMemor // have to use the most conservative estimate possible, which is the maximum length of an instruction multiplied // by the number of instructions in the assembler. int codeMaxLength = MaxInstructionLength * codeAssembler.Instructions.Count; - ulong sizeToReserve = (ulong)(options.GetExpectedGeneratedCodeSize(processMemory.Is64Bit) - + codeMaxLength - + MaxInstructionLength // Extra room for the original instructions - + FarJumpInstructionLength); // Extra room for the jump back to the original code + ulong sizeToReserve = GetSizeToReserveForCodeInjection(processMemory, codeMaxLength, options); // Reserve memory for the injected code, as close as possible to the target instruction var reservationResult = ReserveHookTarget(processMemory, sizeToReserve, targetInstructionAddress, @@ -168,7 +161,9 @@ public static Result Hook(this ProcessMemory processMemor var reservation = reservationResult.Value; // Assemble the code to inject - var assemblingResult = codeAssembler.AssembleToBytes(reservation.Address, 128); + UIntPtr injectedCodeStartAddress = reservation.Address + + (UIntPtr)options.GetExpectedPreCodeSize(processMemory.Is64Bit); + var assemblingResult = codeAssembler.AssembleToBytes(injectedCodeStartAddress, 128); if (assemblingResult.IsFailure) { reservation.Dispose(); @@ -176,7 +171,7 @@ public static Result Hook(this ProcessMemory processMemor } byte[] codeBytes = assemblingResult.Value; - // Resize the reservation now that we have more accurate information + // Resize the reservation now that we know the exact code size int difference = codeMaxLength - codeBytes.Length; if (difference > 0) reservation.Shrink((ulong)difference); @@ -295,6 +290,210 @@ public static Result InsertCodeAt(this ProcessMemory proc UIntPtr targetInstructionAddress, Assembler codeAssembler, params HookRegister[] registersToPreserve) => processMemory.Hook(targetInstructionAddress, codeAssembler, new HookOptions(HookExecutionMode.ExecuteInjectedCodeFirst, registersToPreserve)); + + /// + /// Replaces the instruction or instructions at the address pointed by the given path with the provided code. If the + /// injected code does not fit in the space occupied by the original instructions, a hook will be performed so that + /// the injected code can still be executed instead of the original instructions. + /// This signature uses a byte array containing the code to inject. If your code contains instructions with relative + /// operands (like jumps or calls), they may not point to the intended address. In these cases, prefer the + /// signature. + /// + /// Process memory instance to use. + /// Pointer path to the first byte of the first instruction to replace. + /// + /// Number of consecutive instructions to replace. + /// Assembled code to inject into the target process. + /// Optionally provided registers to be saved and restored around the injected + /// code. This allows the injected code to modify registers without affecting the original code, which could + /// otherwise lead to crashes or unexpected behavior. + /// A result holding either a code change instance that allows you to revert the operation, or a hook + /// failure when the operation failed. If the operation performed a hook, the result will be a + /// that also contains the reservation holding the injected code. + public static Result ReplaceCodeAt(this ProcessMemory processMemory, + PointerPath targetInstructionPointerPath, int instructionCount, byte[] code, + params HookRegister[] registersToPreserve) + { + var addressResult = processMemory.EvaluateMemoryAddress(targetInstructionPointerPath); + if (addressResult.IsFailure) + return new HookFailureOnPathEvaluation(addressResult.Error); + + return processMemory.ReplaceCodeAt(addressResult.Value, instructionCount, code, registersToPreserve); + } + + /// + /// Replaces the instruction or instructions at the given address with the provided code. If the injected code does + /// not fit in the space occupied by the original instructions, a hook will be performed so that the injected code + /// can still be executed instead of the original instructions. + /// This signature uses a byte array containing the code to inject. If your code contains instructions with relative + /// operands (like jumps or calls), they may not point to the intended address. In these cases, prefer the + /// signature. + /// + /// Process memory instance to use. + /// Address of the first byte of the first instruction to replace. + /// Number of consecutive instructions to replace. + /// Assembled code to inject into the target process. + /// Optionally provided registers to be saved and restored around the injected + /// code. This allows the injected code to modify registers without affecting the original code, which could + /// otherwise lead to crashes or unexpected behavior. + /// A result holding either a code change instance that allows you to revert the operation, or a hook + /// failure when the operation failed. If the operation performed a hook, the result will be a + /// that also contains the reservation holding the injected code. + public static Result ReplaceCodeAt(this ProcessMemory processMemory, + UIntPtr targetInstructionAddress, int instructionCount, byte[] code, params HookRegister[] registersToPreserve) + { + if (targetInstructionAddress == UIntPtr.Zero) + return new HookFailureOnZeroPointer(); + if (instructionCount < 1) + return new HookFailureOnInvalidArguments("The number of instructions to replace must be at least 1."); + if (code.Length == 0) + return new HookFailureOnInvalidArguments("The code to inject must contain at least one byte."); + + var hookOptions = new HookOptions(HookExecutionMode.ReplaceOriginalInstruction, registersToPreserve); + + // Attempt to replace the code directly (write the injected code bytes directly on top of the original + // instruction bytes if it can fit). + var fullCodeAssemblyResult = AssembleFullCodeToInject(processMemory, code, targetInstructionAddress, + hookOptions); + if (fullCodeAssemblyResult.IsFailure) + return fullCodeAssemblyResult.Error; + var directReplaceResult = TryReplaceCodeBytes(processMemory, targetInstructionAddress, instructionCount, + fullCodeAssemblyResult.Value); + if (directReplaceResult.IsFailure) + return directReplaceResult.Error; + if (directReplaceResult.Value != null) + return directReplaceResult.Value; + + // The code to inject is larger than the original instructions, so we need to perform a hook + ulong sizeToReserve = GetSizeToReserveForCodeInjection(processMemory, code.Length, hookOptions); + var reservationResult = ReserveHookTarget(processMemory, sizeToReserve, targetInstructionAddress, + hookOptions.JumpMode); + if (reservationResult.IsFailure) + return reservationResult.Error; + var reservation = reservationResult.Value; + + var hookResult = PerformHook(processMemory, targetInstructionAddress, code, reservation, hookOptions); + if (hookResult.IsFailure) + return hookResult.Error; + return hookResult.Value; + } + + /// + /// Replaces the instruction or instructions pointed by the given pointer path with the provided code. If the + /// injected code does not fit in the space occupied by the original instructions, a hook will be performed so that + /// the injected code can still be executed instead of the original instructions. + /// This signature uses an assembler, which is recommended, especially if your code contains instructions with + /// relative operands (like jumps or calls), because the assembler will adjust addresses to guarantee they point to + /// the intended locations. + /// + /// Process memory instance to use. + /// Pointer path to the first byte of the first instruction to replace. + /// + /// Number of consecutive instructions to replace. + /// Code assembler loaded with instructions to inject into the target process. + /// Optionally provided registers to be saved and restored around the injected + /// code. This allows the injected code to modify registers without affecting the original code, which could + /// otherwise lead to crashes or unexpected behavior. + /// A result holding either a code change instance that allows you to revert the operation, or a hook + /// failure when the operation failed. If the operation performed a hook, the result will be a + /// that also contains the reservation holding the injected code. + public static Result ReplaceCodeAt(this ProcessMemory processMemory, + PointerPath targetInstructionPointerPath, int instructionCount, Assembler codeAssembler, + params HookRegister[] registersToPreserve) + { + var addressResult = processMemory.EvaluateMemoryAddress(targetInstructionPointerPath); + if (addressResult.IsFailure) + return new HookFailureOnPathEvaluation(addressResult.Error); + + return processMemory.ReplaceCodeAt(addressResult.Value, instructionCount, codeAssembler, registersToPreserve); + } + + /// + /// Replaces the instruction or instructions at the given address with the provided code. If the injected code does + /// not fit in the space occupied by the original instructions, a hook will be performed so that the injected code + /// can still be executed instead of the original instructions. + /// This signature uses an assembler, which is recommended, especially if your code contains instructions with + /// relative operands (like jumps or calls), because the assembler will adjust addresses to guarantee they point to + /// the intended locations. + /// + /// Process memory instance to use. + /// Address of the first byte of the first instruction to replace. + /// Number of consecutive instructions to replace. + /// Code assembler loaded with instructions to inject into the target process. + /// Optionally provided registers to be saved and restored around the injected + /// code. This allows the injected code to modify registers without affecting the original code, which could + /// otherwise lead to crashes or unexpected behavior. + /// A result holding either a code change instance that allows you to revert the operation, or a hook + /// failure when the operation failed. If the operation performed a hook, the result will be a + /// that also contains the reservation holding the injected code. + public static Result ReplaceCodeAt(this ProcessMemory processMemory, + UIntPtr targetInstructionAddress, int instructionCount, Assembler codeAssembler, + params HookRegister[] registersToPreserve) + { + if (targetInstructionAddress == UIntPtr.Zero) + return new HookFailureOnZeroPointer(); + if (instructionCount < 1) + return new HookFailureOnInvalidArguments("The number of instructions to replace must be at least 1."); + if (codeAssembler.Instructions.Count == 0) + return new HookFailureOnInvalidArguments("The given code assembler must have at least one instruction."); + + var hookOptions = new HookOptions(HookExecutionMode.ReplaceOriginalInstruction, registersToPreserve); + int preCodeSize = hookOptions.GetExpectedPreCodeSize(processMemory.Is64Bit); + + // Attempt to replace the code directly (write the injected code bytes directly on top of the original + // instruction bytes if it can fit). + // For that, we need to assemble the code to inject at the target address. + var targetAddressAssemblyResult = codeAssembler.AssembleToBytes( + targetInstructionAddress + (UIntPtr)preCodeSize, 128); + if (targetAddressAssemblyResult.IsFailure) + return new HookFailureOnCodeAssembly(HookCodeAssemblySource.InjectedCode, targetAddressAssemblyResult.Error); + var fullCodeAssemblyAtTargetAddressResult = AssembleFullCodeToInject(processMemory, + targetAddressAssemblyResult.Value, targetInstructionAddress, hookOptions); + if (fullCodeAssemblyAtTargetAddressResult.IsFailure) + return fullCodeAssemblyAtTargetAddressResult.Error; + byte[] codeAssembledAtTargetAddress = fullCodeAssemblyAtTargetAddressResult.Value; + var directReplaceResult = TryReplaceCodeBytes(processMemory, targetInstructionAddress, instructionCount, + codeAssembledAtTargetAddress); + if (directReplaceResult.IsFailure) + return directReplaceResult.Error; + if (directReplaceResult.Value != null) + return directReplaceResult.Value; + + // The code to inject is larger than the original instructions, so we need to perform a hook + // Reserve memory + int codeMaxLength = MaxInstructionLength * codeAssembler.Instructions.Count; + ulong sizeToReserve = GetSizeToReserveForCodeInjection(processMemory, codeMaxLength, hookOptions); + var reservationResult = ReserveHookTarget(processMemory, sizeToReserve, targetInstructionAddress, + hookOptions.JumpMode); + if (reservationResult.IsFailure) + return reservationResult.Error; + var reservation = reservationResult.Value; + + // Assemble the code to inject (even though we already assembled the code before, it was at a different address, + // which could lead to different instruction bytes). + var injectedCodeStartAddress = reservation.Address + + (UIntPtr)hookOptions.GetExpectedPreCodeSize(processMemory.Is64Bit); + var assemblyResult = codeAssembler.AssembleToBytes(injectedCodeStartAddress, 128); + if (assemblyResult.IsFailure) + { + reservation.Dispose(); + return new HookFailureOnCodeAssembly(HookCodeAssemblySource.InjectedCode, assemblyResult.Error); + } + byte[] code = assemblyResult.Value; + + // Resize the reservation now that we know the exact code size + int difference = codeMaxLength - code.Length; + if (difference > 0) + reservation.Shrink((ulong)difference); + + var hookResult = PerformHook(processMemory, targetInstructionAddress, code, reservation, hookOptions); + if (hookResult.IsFailure) + { + reservation.Dispose(); + return hookResult.Error; + } + return hookResult.Value; + } #endregion @@ -359,46 +558,25 @@ private static Result PerformHook(ProcessMemory processMe jmpBytes = jmpBytes.Concat(Enumerable.Repeat(ProcessMemoryCodeExtensions.NopByte, bytesRead - jmpBytes.Length)).ToArray(); + // Assemble the full code to inject var nextOriginalInstructionAddress = (UIntPtr)(targetInstructionAddress + (ulong)bytesRead); - - // Assemble the pre-code - var preHookCodeResult = BuildPreHookCode(reservation.Address, instructionsToReplace, processMemory.Is64Bit, - options); - if (preHookCodeResult.IsFailure) + var fullCodeResult = AssembleFullCodeToInject(processMemory, code, reservation.Address, options, + instructionsToReplace, nextOriginalInstructionAddress); + if (fullCodeResult.IsFailure) { reservation.Dispose(); - return preHookCodeResult.Error; + return fullCodeResult.Error; } - byte[] preHookCodeBytes = preHookCodeResult.Value; - // Assemble the post-code - var postHookCodeAddress = (UIntPtr)(reservation.Address + (ulong)preHookCodeBytes.Length - + (ulong)code.Length); - var postHookCodeResult = BuildPostHookCode(postHookCodeAddress, instructionsToReplace, processMemory.Is64Bit, - options, nextOriginalInstructionAddress); - if (postHookCodeResult.IsFailure) + byte[] fullCode = fullCodeResult.Value; + if ((ulong)fullCode.Length > reservation.Size) { - reservation.Dispose(); - return postHookCodeResult.Error; + return new HookFailureOnCodeAssembly(HookCodeAssemblySource.Unknown, + $"The assembled code is too large to fit in the reserved memory (reserved {reservation.Size} bytes, but assembled a total of {fullCode.Length} bytes). Please report this, as it is a bug in the hook code generation."); } - byte[] postHookCodeBytes = postHookCodeResult.Value; - - // Assemble the full code to inject - var fullCodeLength = preHookCodeBytes.Length + code.Length + postHookCodeBytes.Length; - if ((ulong)fullCodeLength > reservation.Size) - { - reservation.Dispose(); - return new HookFailureOnCodeAssembly(HookCodeAssemblySource.Unkown, - $"The assembled code is too large to fit in the reserved memory (reserved {reservation.Size} bytes, but assembled a total of {fullCodeLength} bytes). Please report this, as it is a bug in the hook code generation."); - } - var fullInjectedCode = new byte[fullCodeLength]; - Buffer.BlockCopy(preHookCodeBytes, 0, fullInjectedCode, 0, preHookCodeBytes.Length); - Buffer.BlockCopy(code, 0, fullInjectedCode, preHookCodeBytes.Length, code.Length); - Buffer.BlockCopy(postHookCodeBytes, 0, fullInjectedCode, preHookCodeBytes.Length + code.Length, - postHookCodeBytes.Length); // Write the assembled code to the reserved memory - var writeResult = processMemory.WriteBytes(reservation.Address, fullInjectedCode); + var writeResult = processMemory.WriteBytes(reservation.Address, fullCodeResult.Value); if (writeResult.IsFailure) { reservation.Dispose(); @@ -415,6 +593,104 @@ private static Result PerformHook(ProcessMemory processMe return new CodeHook(processMemory, targetInstructionAddress, originalBytesResult.Value, reservation); } + + /// + /// Assembles the full code to inject, including the pre-hook code, the injected code, and the post-hook code. + /// + /// Process memory instance to use. + /// Assembled code to inject into the target process. + /// Address where the injected code will be written. + /// Options defining how the hook behaves. + /// Original instructions that will be replaced by the injected code. These + /// instructions are used to insert original instructions before or after the injected code depending on the hook + /// options, if required at all. If null, no original instructions will be considered. + /// Address of the first byte of the instruction that comes after the original + /// instructions to replace. This is used to generate the jump instruction in the post-hook code. If null, no jump + /// will be assembled. + /// + private static Result AssembleFullCodeToInject(ProcessMemory processMemory, byte[] code, + UIntPtr injectionAddress, HookOptions options, IList? instructionsToReplace = null, + UIntPtr? nextInstructionAddress = null) + { + // Assemble the pre-code + var preHookCodeResult = BuildPreHookCode(injectionAddress, instructionsToReplace, processMemory.Is64Bit, + options); + if (preHookCodeResult.IsFailure) + return preHookCodeResult.Error; + byte[] preHookCodeBytes = preHookCodeResult.Value; + + // Assemble the post-code + var postHookCodeAddress = (UIntPtr)(injectionAddress + (ulong)preHookCodeBytes.Length + (ulong)code.Length); + var postHookCodeResult = BuildPostHookCode(postHookCodeAddress, instructionsToReplace, processMemory.Is64Bit, + options, nextInstructionAddress); + if (postHookCodeResult.IsFailure) + return postHookCodeResult.Error; + byte[] postHookCodeBytes = postHookCodeResult.Value; + + // Assemble the full code to inject + var fullCodeLength = preHookCodeBytes.Length + code.Length + postHookCodeBytes.Length; + var fullInjectedCode = new byte[fullCodeLength]; + Buffer.BlockCopy(preHookCodeBytes, 0, fullInjectedCode, 0, preHookCodeBytes.Length); + Buffer.BlockCopy(code, 0, fullInjectedCode, preHookCodeBytes.Length, code.Length); + Buffer.BlockCopy(postHookCodeBytes, 0, fullInjectedCode, preHookCodeBytes.Length + code.Length, + postHookCodeBytes.Length); + + return fullInjectedCode; + } + + /// + /// Attempts to replace the bytes of the specified number of instructions, starting at the instruction at the given + /// address, with the provided code. If the injected code is larger than the original instructions, the operation + /// cannot be performed and the result will be successful but null, signaling that a hook should be performed + /// instead. + /// + /// Process memory instance to use. + /// Address of the first byte of the first instruction to replace. + /// Number of consecutive instructions to replace. + /// Assembled code to inject into the target process. + /// A result that can hold either a code change instance, or a hook failure when the operation failed. + /// If the operation cannot be performed, the result will be successful but the value will be null. + private static Result TryReplaceCodeBytes(ProcessMemory processMemory, + UIntPtr targetInstructionAddress, int instructionCount, byte[] code) + { + // Read the original instructions to replace to determine how many bytes it makes up. + using var stream = processMemory.GetMemoryStream(targetInstructionAddress); + var codeReader = new StreamCodeReader(stream); + var decoder = Decoder.Create(processMemory.Is64Bit ? 64 : 32, codeReader, targetInstructionAddress); + var instructionsToReplace = new List(); + int bytesRead = 0; + while (instructionsToReplace.Count < instructionCount) + { + var instruction = decoder.Decode(); + if (instruction.IsInvalid) + return new HookFailureOnDecodingFailure(decoder.LastError); + + instructionsToReplace.Add(instruction); + bytesRead += instruction.Length; + } + + // If the instructions to replace are shorter than the injected code, we can't replace them directly + if (bytesRead < code.Length) + return (CodeChange?)null; + + // If we reach this point, we can replace the code bytes. + // Read the original bytes to replace (so that the change can be reverted) + var originalBytesResult = processMemory.ReadBytes(targetInstructionAddress, bytesRead); + if (originalBytesResult.IsFailure) + return new HookFailureOnReadFailure(originalBytesResult.Error); + + // Pad the injected code with NOPs if needed, to avoid leaving bytes from the original instructions in memory + if (bytesRead > code.Length) + code = code.Concat(Enumerable.Repeat(ProcessMemoryCodeExtensions.NopByte, bytesRead - code.Length)) + .ToArray(); + + // Write the injected code directly + var writeResult = processMemory.WriteBytes(targetInstructionAddress, code); + if (writeResult.IsFailure) + return new HookFailureOnWriteFailure(writeResult.Error); + + return new CodeChange(processMemory, targetInstructionAddress, originalBytesResult.Value); + } /// /// Assembles the code that comes before the user-made injected code in a hook operation. @@ -425,13 +701,13 @@ private static Result PerformHook(ProcessMemory processMe /// Options defining how the hook behaves. /// A result holding either the assembled code bytes, or a hook failure. private static Result BuildPreHookCode(ulong baseAddress, - IList instructionsToReplace, bool is64Bit, HookOptions options) + IList? instructionsToReplace, bool is64Bit, HookOptions options) { var assembler = new Assembler(is64Bit ? 64 : 32); // If the hook mode specifies that the original instruction should be executed first, add it to the code if (options.ExecutionMode == HookExecutionMode.ExecuteOriginalInstructionFirst - && instructionsToReplace.Count > 0) + && instructionsToReplace?.Count > 0) { assembler.AddInstruction(instructionsToReplace.First()); } @@ -471,10 +747,11 @@ private static Result BuildPreHookCode(ulong baseAddress, /// Original instructions to be replaced by a jump to the injected code. /// Boolean indicating if the target process is 64-bit. /// Options defining how the hook behaves. - /// Address of the first byte of the instruction to jump back to. + /// Address of the first byte of the instruction to jump back to. If null, + /// the jump back will not be assembled. /// A result holding either the assembled code bytes, or a hook failure. private static Result BuildPostHookCode(ulong baseAddress, - IList instructionsToReplace, bool is64Bit, HookOptions options, UIntPtr originalCodeJumpTarget) + IList? instructionsToReplace, bool is64Bit, HookOptions options, UIntPtr? originalCodeJumpTarget) { var assembler = new Assembler(is64Bit ? 64 : 32); @@ -498,19 +775,20 @@ private static Result BuildPostHookCode(ulong baseAddress, if (options.RegistersToPreserve.Contains(HookRegister.Flags)) assembler.RestoreFlags(); - // If the hook mode specifies that the hook code should be executed first, append the original instructions + // If the hook mode specifies that the injected code should be executed first, append the original instruction if (options.ExecutionMode == HookExecutionMode.ExecuteInjectedCodeFirst - && instructionsToReplace.Count > 0) + && instructionsToReplace?.Count > 0) { assembler.AddInstruction(instructionsToReplace.First()); } // In all cases, add any additional instructions that were replaced by the jump to the injected code - foreach (var instruction in instructionsToReplace.Skip(1)) + foreach (var instruction in instructionsToReplace?.Skip(1) ?? []) assembler.AddInstruction(instruction); // Jump back to the original code - assembler.jmp(originalCodeJumpTarget); + if (originalCodeJumpTarget.HasValue) + assembler.jmp(originalCodeJumpTarget.Value); // Assemble the code and return the resulting bytes var result = assembler.AssembleToBytes(baseAddress, 128); @@ -519,6 +797,21 @@ private static Result BuildPostHookCode(ulong baseAddress, return result.Value; } + + /// + /// Gets the number of bytes to reserve to inject a given code with the specified hook options. + /// + /// Process memory instance to use. + /// Maximum length in bytes of the code instructions to inject. + /// Options defining how the hook behaves. + /// The size to reserve in bytes. + private static ulong GetSizeToReserveForCodeInjection(ProcessMemory processMemory, int codeLength, HookOptions options) + { + return (ulong)(options.GetExpectedGeneratedCodeSize(processMemory.Is64Bit) + + codeLength + + MaxInstructionLength // Extra room for the original instructions + + FarJumpInstructionLength); // Extra room for the jump back to the original code + } /// /// Attempts to reserve executable memory as close as possible to the hook target instruction. diff --git a/src/MindControl.Code/Results/HookFailure.cs b/src/MindControl.Code/Results/HookFailure.cs index 8d1cbb2..dd2da7e 100644 --- a/src/MindControl.Code/Results/HookFailure.cs +++ b/src/MindControl.Code/Results/HookFailure.cs @@ -129,7 +129,7 @@ public record HookFailureOnDecodingFailure(DecoderError Error) public enum HookCodeAssemblySource { /// Default value used when the source is unknown. - Unkown, + Unknown, /// Designates the jump instruction that forwards execution to the injected code. JumpToInjectedCode, /// Designates the code block that is prepended to the injected code. @@ -150,7 +150,19 @@ public record HookFailureOnCodeAssembly(HookCodeAssemblySource Source, string Me { /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() => $"Failed to assemble code: {Message}"; + public override string ToString() => $"Failed to assemble code in {GetSourceAsString(Source)}: {Message}"; + + /// Returns a string representation of the given . + /// The source to convert to a string. + /// A string representation of the given . + public static string GetSourceAsString(HookCodeAssemblySource source) => source switch + { + HookCodeAssemblySource.JumpToInjectedCode => "the jump to the injected code", + HookCodeAssemblySource.PrependedCode => "the code block generated before the injected code", + HookCodeAssemblySource.InjectedCode => "the given code to inject", + HookCodeAssemblySource.AppendedCode => "the code block generated after the injected code", + _ => "an undetermined code block" + }; } /// diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs index ae27de5..95d30df 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs @@ -1,4 +1,5 @@ using Iced.Intel; +using MindControl.Code; using MindControl.Hooks; using MindControl.Results; using NUnit.Framework; @@ -12,6 +13,9 @@ namespace MindControl.Test.ProcessMemoryTests.CodeExtensions; public class ProcessMemoryHookExtensionsTest : ProcessMemoryTest { private const string MovLongValueInstructionBytePattern = "48 B8 DF 54 09 2B BA 3C FD FF"; + // The above pattern corresponds to the MOV instruction that writes the new long value to the RAX register: + // > MOV RAX, 0xFFFD3CBA2B0954DF + // MOV [RCX+20], RAX #region Hook @@ -383,4 +387,385 @@ public void InsertCodeAtWithAssemblerWithPointerPathTest() } #endregion + + #region ReplaceCodeAt + + /// + /// Tests the + /// + /// method. + /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV + /// instruction that assigns a different value. Only one instruction is replaced. + /// Expects the result to be a CodeChange (because the new code is expected to fit in place of the replaced code), + /// and the program output long value to be the one written by the new MOV instruction. + /// + [Test] + public void ReplaceCodeAtWithByteArrayTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + + // Replace the code at the target MOV instruction. + var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, bytes); + Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "1234567890"); // The new value written by the injected code. + } + + /// + /// Tests the + /// + /// method. + /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV + /// instruction that assigns a different value. Two instructions are replaced. + /// Expects the result to be a CodeChange (because the new code is expected to fit in place of the replaced code), + /// and the program output long value to be the original initial value, because the next instruction that actually + /// changes the long value is replaced too, and the replacing code just moves a value to RAX, without actually + /// writing that value anywhere. + /// + [Test] + public void ReplaceCodeAtWithByteArrayOnMultipleInstructionsTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + + // Replace the code at the target MOV instruction. + var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 2, bytes); + Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "-65746876815103"); // The initial value of the target long + } + + /// + /// Tests the + /// + /// method. + /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV + /// instruction that assigns a different value, plus an instruction that sets RCX to zero. Only one instruction is + /// replaced. However, we specify that 4 registers should be preserved, including RCX, which is used by the next + /// original instruction to write the value at the right address. RAX is not preserved. + /// Expects the result to be a CodeHook (because of the register pop and push instructions, the new code should not + /// fit in place of the replaced code), and the program output long value to be the new value written to RAX. + /// Setting RCX to zero should not affect the output because that register should be preserved. + /// + [Test] + public void ReplaceCodeAtWithByteArrayWithPreservedRegistersTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)123); + assembler.mov(new AssemblerRegister64(Register.RCX), (ulong)0); + var bytes = assembler.AssembleToBytes().Value; + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + + // Replace the code at the target MOV instruction. + var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, bytes, + HookRegister.Flags, HookRegister.RcxEcx, HookRegister.RdxEdx, HookRegister.R8); + Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.Value, Is.TypeOf()); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "123"); + } + + /// + /// Tests the + /// + /// method. + /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new sequence + /// of MOV instructions that assign a different value to the same register. Only one instruction is replaced, but + /// the replacing code is expected to be larger than the original instruction. + /// Expects the result to be a CodeHook (because the new code should not fit in place of the replaced code), and the + /// program output long value to be the one written by the new MOV instruction. + /// + [Test] + public void ReplaceCodeAtWithByteArrayWithLargerCodeTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)9991234560); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)8881234560); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)7771234560); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + + // Replace the code at the target MOV instruction. + var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, bytes); + Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.Value, Is.TypeOf()); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "1234567890"); // The new value written by the hook. + } + + /// + /// Tests the + /// + /// method. + /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV + /// instruction that assigns a different value. Only one instruction is replaced. + /// Expects the output long value to be the one written by the new MOV instruction. + /// + [Test] + public void ReplaceCodeAtWithByteArrayWithPointerPathTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + PointerPath pointerPath = movLongAddress.ToString("X"); + + // Replace the code at the target MOV instruction. + var hookResult = TestProcessMemory!.ReplaceCodeAt(pointerPath, 1, bytes); + Assert.That(hookResult.IsSuccess, Is.True); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "1234567890"); // The new value written by the hook. + } + + /// + /// Tests the + /// + /// method. + /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV + /// instruction that assigns a different value. Only one instruction is replaced. + /// Expects the result to be a CodeChange (because the new code is expected to fit in place of the replaced code), + /// and the program output long value to be the one written by the new MOV instruction. + /// + [Test] + public void ReplaceCodeAtWithAssemblerTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + + // Replace the code at the target MOV instruction. + var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, assembler); + Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "1234567890"); // The new value written by the injected code. + } + + /// + /// Tests the + /// + /// method. + /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV + /// instruction that assigns a different value. Two instructions are replaced. + /// Expects the result to be a CodeChange (because the new code is expected to fit in place of the replaced code), + /// and the program output long value to be the original initial value, because the next instruction that actually + /// changes the long value is replaced too, and the replacing code just moves a value to RAX, without actually + /// writing that value anywhere. + /// + [Test] + public void ReplaceCodeAtWithAssemblerOnMultipleInstructionsTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + + // Replace the code at the target MOV instruction. + var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 2, assembler); + Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "-65746876815103"); // The initial value of the target long + } + + /// + /// Tests the + /// + /// method. + /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV + /// instruction that assigns a different value, plus an instruction that sets RCX to zero. Only one instruction is + /// replaced. However, we specify that 4 registers should be preserved, including RCX, which is used by the next + /// original instruction to write the value at the right address. RAX is not preserved. + /// Expects the result to be a CodeHook (because of the register pop and push instructions, the new code should not + /// fit in place of the replaced code), and the program output long value to be the new value written to RAX. + /// Setting RCX to zero should not affect the output because that register should be preserved. + /// + [Test] + public void ReplaceCodeAtWithAssemblerWithPreservedRegistersTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)123); + assembler.mov(new AssemblerRegister64(Register.RCX), (ulong)0); + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + + // Replace the code at the target MOV instruction. + var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, assembler, + HookRegister.Flags, HookRegister.RcxEcx, HookRegister.RdxEdx, HookRegister.R8); + Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.Value, Is.TypeOf()); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "123"); + } + + /// + /// Tests the + /// + /// method. + /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new sequence + /// of MOV instructions that assign a different value to the same register. Only one instruction is replaced, but + /// the replacing code is expected to be larger than the original instruction. + /// Expects the result to be a CodeHook (because the new code should not fit in place of the replaced code), and the + /// program output long value to be the one written by the new MOV instruction. + /// + [Test] + public void ReplaceCodeAtWithAssemblerWithLargerCodeTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)9991234560); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)8881234560); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)7771234560); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + + // Replace the code at the target MOV instruction. + var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, assembler); + Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.Value, Is.TypeOf()); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "1234567890"); // The new value written by the hook. + } + + /// + /// Tests the + /// + /// method. + /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV + /// instruction that assigns a different value. Only one instruction is replaced. + /// Expects the output long value to be the one written by the new MOV instruction. + /// + [Test] + public void ReplaceCodeAtWithAssemblerWithPointerPathTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + PointerPath pointerPath = movLongAddress.ToString("X"); + + // Replace the code at the target MOV instruction. + var hookResult = TestProcessMemory!.ReplaceCodeAt(pointerPath, 1, assembler); + Assert.That(hookResult.IsSuccess, Is.True); + + ProceedUntilProcessEnds(); + AssertFinalResults(5, "1234567890"); // The new value written by the hook. + } + + /// + /// Tests the + /// + /// method, calling it with a pointer path that does not evaluate to a valid address, but otherwise valid + /// parameters. + /// Expects the result to be a . + /// + [Test] + public void ReplaceCodeAtWithByteArrayWithBadPointerPathTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + PointerPath pointerPath = "bad pointer path"; + var hookResult = TestProcessMemory!.ReplaceCodeAt(pointerPath, 1, bytes); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests the + /// + /// method, calling it with a pointer path that does not evaluate to a valid address, but otherwise valid + /// parameters. + /// Expects the result to be a . + /// + [Test] + public void ReplaceCodeAtWithAssemblerWithBadPointerPathTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + PointerPath pointerPath = "bad pointer path"; + var hookResult = TestProcessMemory!.ReplaceCodeAt(pointerPath, 1, assembler); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests the + /// + /// method, calling it with an empty code array, but otherwise valid parameters. + /// Expects the result to be a . + /// + [Test] + public void ReplaceCodeAtWithByteArrayWithNoCodeTest() + { + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, []); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests the + /// + /// method, calling it with an empty assembler, but otherwise valid parameters. + /// Expects the result to be a . + /// + [Test] + public void ReplaceCodeAtWithAssemblerWithNoCodeTest() + { + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, new Assembler(64)); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests the + /// + /// method, calling it with a number of instructions to replace of 0, but otherwise valid parameters. + /// Expects the result to be a . + /// + [Test] + public void ReplaceCodeAtWithByteArrayWithZeroInstructionTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var bytes = assembler.AssembleToBytes().Value; + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 0, bytes); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests the + /// + /// method, calling it with a number of instructions to replace of 0, but otherwise valid parameters. + /// Expects the result to be a . + /// + [Test] + public void ReplaceCodeAtWithAssemblerWithZeroInstructionTest() + { + var assembler = new Assembler(64); + assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 0, assembler); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + #endregion } \ No newline at end of file From 8232c200235d22352ea52ccb60f6e81ad8459cf1 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Thu, 20 Jun 2024 21:27:18 +0200 Subject: [PATCH 27/66] Upgraded Test.TargetApp to .net 8 --- .../Code/ProcessMemory.Code.cs | 4 +- src/MindControl/Results/WriteFailure.cs | 2 +- .../MindControl.Test.TargetApp.csproj | 7 ++- .../BaseProcessMemoryCodeExtensionTest.cs | 25 ++++++++ .../ProcessMemoryCodeExtensionsTest.cs | 24 ++++---- .../ProcessMemoryHookExtensionsTest.cs | 61 +++++++++---------- 6 files changed, 75 insertions(+), 48 deletions(-) create mode 100644 test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs diff --git a/src/MindControl.Code/Code/ProcessMemory.Code.cs b/src/MindControl.Code/Code/ProcessMemory.Code.cs index d25aa28..5731551 100644 --- a/src/MindControl.Code/Code/ProcessMemory.Code.cs +++ b/src/MindControl.Code/Code/ProcessMemory.Code.cs @@ -76,7 +76,9 @@ public static Result DisableCodeAt(this ProcessM var nopInstructions = new byte[fullLength]; nopInstructions.AsSpan().Fill(NopByte); - processMemory.WriteBytes(address, nopInstructions); + var writeResult = processMemory.WriteBytes(address, nopInstructions, MemoryProtectionStrategy.Remove); + if (writeResult.IsFailure) + return new CodeWritingFailureOnWriteFailure(writeResult.Error); return new CodeChange(processMemory, address, originalBytesResult.Value); } diff --git a/src/MindControl/Results/WriteFailure.cs b/src/MindControl/Results/WriteFailure.cs index ec69b8e..991e992 100644 --- a/src/MindControl/Results/WriteFailure.cs +++ b/src/MindControl/Results/WriteFailure.cs @@ -98,7 +98,7 @@ public record WriteFailureOnSystemProtectionRemoval(UIntPtr Address, SystemFailu /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"Failed to remove the protection of the memory at address {Address}: {Details}.{Environment.NewLine}Change the memory protection strategy to {nameof(MemoryProtectionStrategy)}.{nameof(MemoryProtectionStrategy.Ignore)} to prevent memory protection removal. As protection removal is the first step when writing a value, it may simply be that the provided target address does not point to valid memory."; + => $"Failed to remove the protection of the memory at address {Address:X}: \"{Details}\".{Environment.NewLine}Change the memory protection strategy to {nameof(MemoryProtectionStrategy)}.{nameof(MemoryProtectionStrategy.Ignore)} to prevent memory protection removal. As protection removal is the first step when writing a value, it may simply be that the provided target address does not point to valid memory."; } /// diff --git a/test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj b/test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj index 0d33ea2..baa28d3 100644 --- a/test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj +++ b/test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj @@ -2,11 +2,16 @@ Exe - net6.0 + net8.0 false enable enable True + + x86 + MindControl.Test.TargetApp.x86 + + diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs new file mode 100644 index 0000000..a95fb36 --- /dev/null +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs @@ -0,0 +1,25 @@ +namespace MindControl.Test.ProcessMemoryTests.CodeExtensions; + +/// +/// Base class for tests of the class related to code manipulation. +/// Provides methods and properties related to code manipulation. +/// +public abstract class BaseProcessMemoryCodeExtensionTest : ProcessMemoryTest +{ + /// + /// Finds and returns the address of the MOV instruction that loads the new long value in the target app into the + /// RAX register, before assigning it to the output long value. + /// + protected UIntPtr FindMovLongAddress() + { + // Only search executable memory for two reasons: + // - It is faster. + // - When built on .net 8, there are 2 instances of the code. Only the right one is in executable memory. + + return TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF", + settings: new FindBytesSettings { SearchExecutable = true }).First(); + + // > MOV RAX, 0xFFFD3CBA2B0954DF ; Loads the new long value into the RAX register. This is the one we get. + // MOV [RCX+20], RAX ; Changes the long value in the class instance that is output at the end. + } +} \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs index 29a99c8..aec6ce0 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs @@ -8,7 +8,7 @@ namespace MindControl.Test.ProcessMemoryTests.CodeExtensions; /// Tests the features of the class related to code manipulation. /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryCodeExtensionsTest : ProcessMemoryTest +public class ProcessMemoryCodeExtensionsTest : BaseProcessMemoryCodeExtensionTest { /// /// Tests the method. @@ -20,12 +20,12 @@ public class ProcessMemoryCodeExtensionsTest : ProcessMemoryTest [Test] public void DisableCodeAtTest() { - var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First() + 10; - var result = TestProcessMemory.DisableCodeAt(movLongAddress); + var movLongAddress = FindMovLongAddress() + 10; + var result = TestProcessMemory!.DisableCodeAt(movLongAddress); Assert.That(result.IsSuccess, Is.True); Assert.That(result.Value.Address, Is.EqualTo(movLongAddress)); Assert.That(result.Value.Length, Is.AtLeast(1)); // We don't care how long it is but we check that it is set. - + ProceedUntilProcessEnds(); // Test that the output long at index 5 is the first value set when the program starts, not the one that was @@ -40,9 +40,9 @@ public void DisableCodeAtTest() [Test] public void DisableCodeAtWithPointerPathTest() { - var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First() + 10; + var movLongAddress = FindMovLongAddress() + 10; var pointerPath = movLongAddress.ToString("X"); - var result = TestProcessMemory.DisableCodeAt(pointerPath); + var result = TestProcessMemory!.DisableCodeAt(pointerPath); Assert.That(result.IsSuccess, Is.True); ProceedUntilProcessEnds(); @@ -59,8 +59,8 @@ public void DisableCodeAtWithPointerPathTest() [Test] public void DisableCodeAtWithMultipleInstructionsTest() { - var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First(); - var result = TestProcessMemory.DisableCodeAt(movLongAddress, 5); + var movLongAddress = FindMovLongAddress(); + var result = TestProcessMemory!.DisableCodeAt(movLongAddress, 5); Assert.That(result.IsSuccess, Is.True); ProceedUntilProcessEnds(); @@ -82,8 +82,8 @@ public void DisableCodeAtWithMultipleInstructionsTest() [Test] public void DisableCodeAtRevertTest() { - var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First() + 10; - var result = TestProcessMemory.DisableCodeAt(movLongAddress); + var movLongAddress = FindMovLongAddress() + 10; + var result = TestProcessMemory!.DisableCodeAt(movLongAddress); result.Value.Revert(); ProceedUntilProcessEnds(); @@ -111,8 +111,8 @@ public void DisableCodeAtWithZeroAddressTest() [Test] public void DisableCodeAtWithInvalidInstructionCountTest() { - var movLongAddress = TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF").First() + 10; - var result = TestProcessMemory.DisableCodeAt(movLongAddress, 0); + var movLongAddress = FindMovLongAddress() + 10; + var result = TestProcessMemory!.DisableCodeAt(movLongAddress, 0); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf()); } diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs index 95d30df..a18a36b 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs @@ -10,13 +10,8 @@ namespace MindControl.Test.ProcessMemoryTests.CodeExtensions; /// Tests the features of the class related to code hooks. /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryHookExtensionsTest : ProcessMemoryTest +public class ProcessMemoryHookExtensionsTest : BaseProcessMemoryCodeExtensionTest { - private const string MovLongValueInstructionBytePattern = "48 B8 DF 54 09 2B BA 3C FD FF"; - // The above pattern corresponds to the MOV instruction that writes the new long value to the RAX register: - // > MOV RAX, 0xFFFD3CBA2B0954DF - // MOV [RCX+20], RAX - #region Hook /// @@ -32,7 +27,7 @@ public void HookAndReplaceMovWithByteArrayTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, @@ -65,7 +60,7 @@ public void HookAndInsertMovWithRegisterIsolationWithByteArrayTest() assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Hook the instruction that writes the long value to RAX, to append code that writes another value. // Specify that the RAX register should be isolated. @@ -91,7 +86,7 @@ public void HookAndReplaceMovWithAssemblerTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. var hookResult = TestProcessMemory!.Hook(movLongAddress, assembler, @@ -123,7 +118,7 @@ public void HookAndInsertMovWithRegisterIsolationWithAssemblerTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Hook the instruction that writes the long value to RAX, to append code that writes another value. // Specify that the RAX register should be isolated. @@ -192,7 +187,7 @@ public void HookWithBadPathWithAssemblerTest() [Test] public void HookWithEmptyCodeArrayTest() { - var address = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var address = FindMovLongAddress(); var hookResult = TestProcessMemory!.Hook(address, [], new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsFailure, Is.True); @@ -207,7 +202,7 @@ public void HookWithEmptyCodeArrayTest() [Test] public void HookWithEmptyAssemblerTest() { - var address = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var address = FindMovLongAddress(); var assembler = new Assembler(64); var hookResult = TestProcessMemory!.Hook(address, assembler, new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); @@ -228,7 +223,7 @@ public void HookRevertTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, @@ -256,7 +251,7 @@ public void InsertCodeAtWithByteArrayTest() assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; var movLongNextInstructionAddress = - TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First() + 10; + FindMovLongAddress() + 10; // Insert the code right after our target MOV instruction. // That way, the RAX register will be set to the value we want before it's used to write the new long value. @@ -286,7 +281,7 @@ public void InsertCodeAtWithByteArrayWithIsolationTest() assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; var movLongNextInstructionAddress = - TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First() + 10; + FindMovLongAddress() + 10; // Insert the code right after our target MOV instruction. // That way, the RAX register will be set to the value we want before it's used to write the new long value. @@ -311,7 +306,7 @@ public void InsertCodeAtWithAssemblerTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var movLongNextInstructionAddress = - TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First() + 10; + FindMovLongAddress() + 10; var hookResult = TestProcessMemory!.InsertCodeAt(movLongNextInstructionAddress, assembler); Assert.That(hookResult.IsSuccess, Is.True); @@ -332,7 +327,7 @@ public void InsertCodeAtWithAssemblerWithIsolationTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var movLongNextInstructionAddress = - TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First() + 10; + FindMovLongAddress() + 10; var hookResult = TestProcessMemory!.InsertCodeAt(movLongNextInstructionAddress, assembler, HookRegister.RaxEax); Assert.That(hookResult.IsSuccess, Is.True); @@ -353,7 +348,7 @@ public void InsertCodeAtWithByteArrayWithPointerPathTest() assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; var movLongNextInstructionAddress = - TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First() + 10; + FindMovLongAddress() + 10; var pointerPath = movLongNextInstructionAddress.ToString("X"); var hookResult = TestProcessMemory!.InsertCodeAt(pointerPath, bytes); @@ -376,7 +371,7 @@ public void InsertCodeAtWithAssemblerWithPointerPathTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var movLongNextInstructionAddress = - TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First() + 10; + FindMovLongAddress() + 10; var pointerPath = movLongNextInstructionAddress.ToString("X"); var hookResult = TestProcessMemory!.InsertCodeAt(pointerPath, assembler); @@ -405,7 +400,7 @@ public void ReplaceCodeAtWithByteArrayTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Replace the code at the target MOV instruction. var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, bytes); @@ -433,7 +428,7 @@ public void ReplaceCodeAtWithByteArrayOnMultipleInstructionsTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Replace the code at the target MOV instruction. var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 2, bytes); @@ -463,7 +458,7 @@ public void ReplaceCodeAtWithByteArrayWithPreservedRegistersTest() assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)123); assembler.mov(new AssemblerRegister64(Register.RCX), (ulong)0); var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Replace the code at the target MOV instruction. var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, bytes, @@ -494,7 +489,7 @@ public void ReplaceCodeAtWithByteArrayWithLargerCodeTest() assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)7771234560); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Replace the code at the target MOV instruction. var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, bytes); @@ -519,7 +514,7 @@ public void ReplaceCodeAtWithByteArrayWithPointerPathTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); PointerPath pointerPath = movLongAddress.ToString("X"); // Replace the code at the target MOV instruction. @@ -544,7 +539,7 @@ public void ReplaceCodeAtWithAssemblerTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Replace the code at the target MOV instruction. var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, assembler); @@ -571,7 +566,7 @@ public void ReplaceCodeAtWithAssemblerOnMultipleInstructionsTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Replace the code at the target MOV instruction. var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 2, assembler); @@ -600,7 +595,7 @@ public void ReplaceCodeAtWithAssemblerWithPreservedRegistersTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)123); assembler.mov(new AssemblerRegister64(Register.RCX), (ulong)0); - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Replace the code at the target MOV instruction. var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, assembler, @@ -630,7 +625,7 @@ public void ReplaceCodeAtWithAssemblerWithLargerCodeTest() assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)8881234560); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)7771234560); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); // Replace the code at the target MOV instruction. var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, assembler); @@ -654,7 +649,7 @@ public void ReplaceCodeAtWithAssemblerWithPointerPathTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); PointerPath pointerPath = movLongAddress.ToString("X"); // Replace the code at the target MOV instruction. @@ -711,7 +706,7 @@ public void ReplaceCodeAtWithAssemblerWithBadPointerPathTest() [Test] public void ReplaceCodeAtWithByteArrayWithNoCodeTest() { - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, []); Assert.That(hookResult.IsSuccess, Is.False); Assert.That(hookResult.Error, Is.TypeOf()); @@ -726,7 +721,7 @@ public void ReplaceCodeAtWithByteArrayWithNoCodeTest() [Test] public void ReplaceCodeAtWithAssemblerWithNoCodeTest() { - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, new Assembler(64)); Assert.That(hookResult.IsSuccess, Is.False); Assert.That(hookResult.Error, Is.TypeOf()); @@ -744,7 +739,7 @@ public void ReplaceCodeAtWithByteArrayWithZeroInstructionTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 0, bytes); Assert.That(hookResult.IsSuccess, Is.False); Assert.That(hookResult.Error, Is.TypeOf()); @@ -761,7 +756,7 @@ public void ReplaceCodeAtWithAssemblerWithZeroInstructionTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = TestProcessMemory!.FindBytes(MovLongValueInstructionBytePattern).First(); + var movLongAddress = FindMovLongAddress(); var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 0, assembler); Assert.That(hookResult.IsSuccess, Is.False); Assert.That(hookResult.Error, Is.TypeOf()); From 773ea56f7e4548b526b3303f0f32dab4f57133ee Mon Sep 17 00:00:00 2001 From: Doublevil Date: Fri, 21 Jun 2024 18:06:11 +0200 Subject: [PATCH 28/66] First x86 tests and fixes --- MindControl.sln | 11 +++ .../Native/IOperatingSystemService.cs | 4 +- src/MindControl/Native/Win32Service.cs | 8 +- .../ProcessMemory/ProcessMemory.Allocation.cs | 2 +- .../ProcessMemory/ProcessMemory.FindBytes.cs | 2 +- .../ProcessMemory/ProcessMemory.Read.cs | 23 ++++-- .../ProcessMemory/ProcessMemory.Write.cs | 8 +- src/MindControl/Results/WriteFailure.cs | 2 +- .../MindControl.Test.TargetApp.csproj | 1 - .../ProcessMemoryStreamTest.cs | 2 +- .../AllocationTests/MemoryAllocationTest.cs | 2 +- .../AllocationTests/MemoryReservationTest.cs | 2 +- test/MindControl.Test/MindControl.Test.csproj | 28 ++++--- ...MemoryTest.cs => BaseProcessMemoryTest.cs} | 74 +++++++++++++++++-- .../BaseProcessMemoryCodeExtensionTest.cs | 2 +- .../ProcessMemoryAllocationTest.cs | 2 +- .../ProcessMemoryAttachTest.cs | 2 +- .../ProcessMemoryEvaluateTest.cs | 2 +- .../ProcessMemoryFindBytesTest.cs | 2 +- .../ProcessMemoryInjectionTest.cs | 25 +++++-- .../ProcessMemoryReadTest.cs | 2 +- .../ProcessMemoryWriteTest.cs | 48 ++++++------ .../ProcessMemoryTests/ProcessTrackerTest.cs | 2 +- 23 files changed, 183 insertions(+), 73 deletions(-) rename test/MindControl.Test/ProcessMemoryTests/{ProcessMemoryTest.cs => BaseProcessMemoryTest.cs} (52%) diff --git a/MindControl.sln b/MindControl.sln index 27c4af0..3b2842e 100644 --- a/MindControl.sln +++ b/MindControl.sln @@ -19,32 +19,43 @@ Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A13C976F-5866-48C4-904E-2C1960220FAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A13C976F-5866-48C4-904E-2C1960220FAC}.Debug|Any CPU.Build.0 = Debug|Any CPU {A13C976F-5866-48C4-904E-2C1960220FAC}.Release|Any CPU.ActiveCfg = Release|Any CPU {A13C976F-5866-48C4-904E-2C1960220FAC}.Release|Any CPU.Build.0 = Release|Any CPU + {A13C976F-5866-48C4-904E-2C1960220FAC}.Debug|x86.ActiveCfg = Debug|Any CPU {2BE5C902-70EC-4DBE-B782-FB0623518F96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2BE5C902-70EC-4DBE-B782-FB0623518F96}.Debug|Any CPU.Build.0 = Debug|Any CPU {2BE5C902-70EC-4DBE-B782-FB0623518F96}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BE5C902-70EC-4DBE-B782-FB0623518F96}.Release|Any CPU.Build.0 = Release|Any CPU + {2BE5C902-70EC-4DBE-B782-FB0623518F96}.Debug|x86.ActiveCfg = Debug|Any CPU {9FE5EF8E-1230-42DB-A6B2-0C2633D32A1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9FE5EF8E-1230-42DB-A6B2-0C2633D32A1F}.Debug|Any CPU.Build.0 = Debug|Any CPU {9FE5EF8E-1230-42DB-A6B2-0C2633D32A1F}.Release|Any CPU.ActiveCfg = Release|Any CPU {9FE5EF8E-1230-42DB-A6B2-0C2633D32A1F}.Release|Any CPU.Build.0 = Release|Any CPU + {9FE5EF8E-1230-42DB-A6B2-0C2633D32A1F}.Debug|x86.ActiveCfg = Debug|x86 + {9FE5EF8E-1230-42DB-A6B2-0C2633D32A1F}.Debug|x86.Build.0 = Debug|x86 + {9FE5EF8E-1230-42DB-A6B2-0C2633D32A1F}.Release|x86.ActiveCfg = Release|x86 + {9FE5EF8E-1230-42DB-A6B2-0C2633D32A1F}.Release|x86.Build.0 = Release|x86 {AA8C9AF6-7C31-43D1-A519-DBB1C3252127}.Debug|Any CPU.ActiveCfg = Debug|x64 {AA8C9AF6-7C31-43D1-A519-DBB1C3252127}.Debug|Any CPU.Build.0 = Debug|x64 {AA8C9AF6-7C31-43D1-A519-DBB1C3252127}.Release|Any CPU.ActiveCfg = Release|x64 {AA8C9AF6-7C31-43D1-A519-DBB1C3252127}.Release|Any CPU.Build.0 = Release|x64 + {AA8C9AF6-7C31-43D1-A519-DBB1C3252127}.Debug|x86.ActiveCfg = Debug|Win32 {5C1B693D-D176-41B2-A47A-E78E098171CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5C1B693D-D176-41B2-A47A-E78E098171CB}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C1B693D-D176-41B2-A47A-E78E098171CB}.Release|Any CPU.ActiveCfg = Release|Any CPU {5C1B693D-D176-41B2-A47A-E78E098171CB}.Release|Any CPU.Build.0 = Release|Any CPU + {5C1B693D-D176-41B2-A47A-E78E098171CB}.Debug|x86.ActiveCfg = Debug|Any CPU {BF166555-9220-42C6-A93E-EC9FBA9AC38C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BF166555-9220-42C6-A93E-EC9FBA9AC38C}.Debug|Any CPU.Build.0 = Debug|Any CPU {BF166555-9220-42C6-A93E-EC9FBA9AC38C}.Release|Any CPU.ActiveCfg = Release|Any CPU {BF166555-9220-42C6-A93E-EC9FBA9AC38C}.Release|Any CPU.Build.0 = Release|Any CPU + {BF166555-9220-42C6-A93E-EC9FBA9AC38C}.Debug|x86.ActiveCfg = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/MindControl/Native/IOperatingSystemService.cs b/src/MindControl/Native/IOperatingSystemService.cs index df54f54..42529f3 100644 --- a/src/MindControl/Native/IOperatingSystemService.cs +++ b/src/MindControl/Native/IOperatingSystemService.cs @@ -147,10 +147,8 @@ Result CreateRemoteThread(IntPtr processHandle, UIntPtr s /// /// Handle of the target process. /// Base address of the target memory region. - /// A boolean indicating if the target process is 64-bit or not. /// A result holding either the metadata of the target memory region, or a system failure. - Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress, - bool is64Bit); + Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress); /// /// Gets the allocation granularity (minimal allocation size) of the system. diff --git a/src/MindControl/Native/Win32Service.cs b/src/MindControl/Native/Win32Service.cs index d98758f..0654158 100644 --- a/src/MindControl/Native/Win32Service.cs +++ b/src/MindControl/Native/Win32Service.cs @@ -198,7 +198,7 @@ public Result ReadProcessMemoryPartial(IntPtr processHandl UIntPtr currentAddress = range.Start; while (currentAddress.ToUInt64() <= rangeEnd) { - var getRegionResult = GetRegionMetadata(processHandle, currentAddress, is64Bit); + var getRegionResult = GetRegionMetadata(processHandle, currentAddress); // If we failed to get the region metadata, stop iterating. if (getRegionResult.IsFailure) @@ -450,13 +450,11 @@ public MemoryRange GetFullMemoryRange() /// /// Handle of the target process. /// Base address of the target memory region. - /// A boolean indicating if the target process is 64-bit or not. /// A result holding either the metadata of the target memory region, or a system failure. - public Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress, - bool is64Bit) + public Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress) { MemoryBasicInformation memoryBasicInformation; - if (is64Bit) + if (IsSystem64Bit()) { // Use the 64-bit variant of the structure. var memInfo64 = new MemoryBasicInformation64(); diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index 9e489ca..7f33a1c 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -97,7 +97,7 @@ private Result FindAndAllocateFreeMemory(ulong s MemoryRangeMetadata currentMetadata; while ((nextAddress.ToUInt64() <= actualRange.Value.End.ToUInt64() && nextAddress.ToUInt64() >= actualRange.Value.Start.ToUInt64()) - && (currentMetadata = _osService.GetRegionMetadata(ProcessHandle, nextAddress, Is64Bit) + && (currentMetadata = _osService.GetRegionMetadata(ProcessHandle, nextAddress) .GetValueOrDefault()).Size.ToUInt64() > 0) { nextAddressForward = (UIntPtr)Math.Max(nextAddressForward.ToUInt64(), diff --git a/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs b/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs index a279152..cc758fe 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs @@ -182,7 +182,7 @@ private MemoryRange[] GetAggregatedRegionRanges(MemoryRange range, FindBytesSett UIntPtr currentAddress = range.Start; while (currentAddress.ToUInt64() <= rangeEnd) { - var getRegionResult = _osService.GetRegionMetadata(ProcessHandle, currentAddress, Is64Bit); + var getRegionResult = _osService.GetRegionMetadata(ProcessHandle, currentAddress); if (getRegionResult.IsFailure) { // If we failed to get the region metadata, we cannot continue because we don't know where the next diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs index 37cefd1..151cabf 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs @@ -160,13 +160,20 @@ public Result Read(UIntPtr address) where T : struct // Get the size of the target type int size; - try - { - size = Marshal.SizeOf(); - } - catch (ArgumentException) + + // Exception for UIntPtr to use the size of the attached process platform, not the system platform + if (typeof(T) == typeof(UIntPtr)) + size = Is64Bit ? 8 : 4; + else { - return new ReadFailureOnConversionFailure(); + try + { + size = Marshal.SizeOf(); + } + catch (ArgumentException) + { + return new ReadFailureOnConversionFailure(); + } } // Read the bytes from the process memory @@ -178,6 +185,10 @@ public Result Read(UIntPtr address) where T : struct // Convert the bytes into the target type try { + // Exception for UIntPtr to use the size of the attached process platform, not the system platform + if (typeof(T) == typeof(UIntPtr) && !Is64Bit) + return (T)(object)new UIntPtr(BitConverter.ToUInt32(bytes, 0)); + return MemoryMarshal.Read(bytes); } catch (Exception) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs index e81128d..0a52256 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs @@ -106,7 +106,11 @@ public Result Write(UIntPtr address, T value, { if (value == null) throw new ArgumentNullException(nameof(value), "The value to write cannot be null."); - + if (value is IntPtr ptr && ptr.ToInt64() > uint.MaxValue) + return new WriteFailureOnIncompatibleBitness((UIntPtr)ptr); + if (value is UIntPtr uptr && uptr.ToUInt64() > uint.MaxValue) + return new WriteFailureOnIncompatibleBitness(uptr); + return value switch { bool v => WriteBool(address, v, memoryProtectionStrategy), @@ -116,6 +120,8 @@ public Result Write(UIntPtr address, T value, int v => WriteInt(address, v, memoryProtectionStrategy), uint v => WriteUInt(address, v, memoryProtectionStrategy), IntPtr v => WriteIntPtr(address, v, memoryProtectionStrategy), + UIntPtr v => Is64Bit ? WriteULong(address, v.ToUInt64(), memoryProtectionStrategy) + : WriteUInt(address, (uint)v.ToUInt64(), memoryProtectionStrategy), float v => WriteFloat(address, v, memoryProtectionStrategy), long v => WriteLong(address, v, memoryProtectionStrategy), ulong v => WriteULong(address, v, memoryProtectionStrategy), diff --git a/src/MindControl/Results/WriteFailure.cs b/src/MindControl/Results/WriteFailure.cs index 991e992..3b22be6 100644 --- a/src/MindControl/Results/WriteFailure.cs +++ b/src/MindControl/Results/WriteFailure.cs @@ -72,7 +72,7 @@ public record WriteFailureOnIncompatibleBitness(UIntPtr Address) /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"The address to write, {Address}, is a 64-bit address, but the target process is 32-bit."; + => $"The pointer to write, {Address}, is too large for a 32-bit process. If you want to write an 8-byte value and not a memory address, use a ulong instead."; } /// diff --git a/test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj b/test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj index baa28d3..40018b0 100644 --- a/test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj +++ b/test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj @@ -11,7 +11,6 @@ x86 - MindControl.Test.TargetApp.x86 diff --git a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs index 9ee1284..cc35660 100644 --- a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs +++ b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs @@ -11,7 +11,7 @@ namespace MindControl.Test.AddressingTests; /// actual instance of and depend on that method. /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryStreamTest : ProcessMemoryTest +public class ProcessMemoryStreamTest : BaseProcessMemoryTest { /// /// Tests the method. diff --git a/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs b/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs index 74fb70f..369d65d 100644 --- a/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs +++ b/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs @@ -12,7 +12,7 @@ namespace MindControl.Test.AllocationTests; /// depend on that method. /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class MemoryAllocationTest : ProcessMemoryTest +public class MemoryAllocationTest : BaseProcessMemoryTest { private MemoryAllocation _allocation; diff --git a/test/MindControl.Test/AllocationTests/MemoryReservationTest.cs b/test/MindControl.Test/AllocationTests/MemoryReservationTest.cs index 48842e9..756fe9a 100644 --- a/test/MindControl.Test/AllocationTests/MemoryReservationTest.cs +++ b/test/MindControl.Test/AllocationTests/MemoryReservationTest.cs @@ -10,7 +10,7 @@ namespace MindControl.Test.AllocationTests; /// actual instance of and depend on that method. /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class MemoryReservationTest : ProcessMemoryTest +public class MemoryReservationTest : BaseProcessMemoryTest { private MemoryAllocation _allocation; private MemoryReservation _reservation; diff --git a/test/MindControl.Test/MindControl.Test.csproj b/test/MindControl.Test/MindControl.Test.csproj index 66e5404..cb850ff 100644 --- a/test/MindControl.Test/MindControl.Test.csproj +++ b/test/MindControl.Test/MindControl.Test.csproj @@ -27,16 +27,24 @@ - - - false - Content - PreserveNewest - Release - x64 - true - + + + + + + + + + + diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs b/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs similarity index 52% rename from test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs rename to test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs index 6b1d18c..ac5f0ae 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs @@ -6,12 +6,12 @@ namespace MindControl.Test.ProcessMemoryTests; /// -/// Tests 's general features. -/// For memory reading tests, see . -/// For memory path evaluation tests, see . +/// Base class for tests that use a . +/// Executes a TargetApp process and provides a instance, along with methods to manipulate +/// the process and general test helpers around the target app. /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryTest +public class BaseProcessMemoryTest { /// Name of the main module of the target app. protected const string MainModuleName = "MindControl.Test.TargetApp.dll"; @@ -25,6 +25,9 @@ public class ProcessMemoryTest private Process? _targetProcess; protected ProcessMemory? TestProcessMemory; protected UIntPtr OuterClassPointer; + + /// Gets a boolean value defining which version of the target app is used. + protected virtual bool Is64Bit => true; /// /// Initializes the necessary instances for the tests. @@ -32,7 +35,7 @@ public class ProcessMemoryTest [SetUp] public void Initialize() { - _targetProcess = StartTargetAppProcess(); + _targetProcess = StartTargetAppProcess(Is64Bit); string? line = _targetProcess.StandardOutput.ReadLine(); if (!UIntPtr.TryParse(line, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out OuterClassPointer)) throw new Exception($"Could not read the outer class pointer output by the app: \"{line}\"."); @@ -43,13 +46,14 @@ public void Initialize() /// /// Starts the target app and returns its process. /// - public static Process StartTargetAppProcess() + /// Whether to run the 64-bit version of the target app. Default is true. + public static Process StartTargetAppProcess(bool run64BitVersion = true) { var process = new Process { StartInfo = new ProcessStartInfo { - FileName = "./MindControl.Test.TargetApp.exe", + FileName = $"./TargetApp/{(run64BitVersion ? "x64" : "x86")}/MindControl.Test.TargetApp.exe", RedirectStandardOutput = true, RedirectStandardInput = true, StandardOutputEncoding = Encoding.UTF8 @@ -131,6 +135,62 @@ protected void ProceedUntilProcessEnds() "-99879416311.4478", "85,102,119,136" ]; + + /// Index, in the target app output, of the outer class instance bool value. + protected const int IndexOfOutputOuterBool = 0; + /// Index, in the target app output, of the outer class instance byte value. + protected const int IndexOfOutputOuterByte = 1; + /// Index, in the target app output, of the outer class instance int value. + protected const int IndexOfOutputOuterInt = 2; + /// Index, in the target app output, of the outer class instance uint value. + protected const int IndexOfOutputOuterUint = 3; + /// Index, in the target app output, of the outer class instance string value. + protected const int IndexOfOutputOuterString = 4; + /// Index, in the target app output, of the outer class instance long value. + protected const int IndexOfOutputOuterLong = 5; + /// Index, in the target app output, of the outer class instance ulong value. + protected const int IndexOfOutputOuterUlong = 6; + /// Index, in the target app output, of the inner class instance byte value. + protected const int IndexOfOutputInnerByte = 7; + /// Index, in the target app output, of the inner class instance int value. + protected const int IndexOfOutputInnerInt = 8; + /// Index, in the target app output, of the inner class instance long value. + protected const int IndexOfOutputInnerLong = 9; + /// Index, in the target app output, of the outer class instance short value. + protected const int IndexOfOutputOuterShort = 10; + /// Index, in the target app output, of the outer class instance ushort value. + protected const int IndexOfOutputOuterUshort = 11; + /// Index, in the target app output, of the outer class instance float value. + protected const int IndexOfOutputOuterFloat = 12; + /// Index, in the target app output, of the outer class instance double value. + protected const int IndexOfOutputOuterDouble = 13; + /// Index, in the target app output, of the outer class instance byte array value. + protected const int IndexOfOutputOuterByteArray = 14; + + /// + /// Gets the pointer path for the value at the specified index by order of output of the target app. + /// + /// Index of the value to get the pointer path for, by order of output of the target app. + /// + /// Pointer path to the value at the specified index. + /// Thrown if the index is not recognized. + protected PointerPath GetPointerPathForValueAtIndex(int index) => OuterClassPointer.ToString("X") + index switch + { + IndexOfOutputOuterBool => Is64Bit ? "+48" : "+38", + IndexOfOutputOuterByte => Is64Bit ? "+49" : "+39", + IndexOfOutputOuterInt => Is64Bit ? "+38" : "+28", + IndexOfOutputOuterUint => Is64Bit ? "+3C" : "+2C", + IndexOfOutputOuterString => Is64Bit ? "+8" : "+1C", + IndexOfOutputOuterLong => Is64Bit ? "+20" : "+4", + IndexOfOutputOuterUlong => Is64Bit ? "+28" : "+C", + IndexOfOutputInnerLong => Is64Bit ? "+10,8" : "+20,4", + IndexOfOutputOuterShort => Is64Bit ? "+44" : "+34", + IndexOfOutputOuterUshort => Is64Bit ? "+46" : "+36", + IndexOfOutputOuterFloat => Is64Bit ? "+40" : "+30", + IndexOfOutputOuterDouble => Is64Bit ? "+30" : "+14", + IndexOfOutputOuterByteArray => Is64Bit ? "+18,10" : "+24,8", + _ => throw new ArgumentOutOfRangeException(nameof(index)) + }; /// /// Asserts that among the final results output by the target app, the one at the given index matches the diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs index a95fb36..21be665 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs @@ -4,7 +4,7 @@ /// Base class for tests of the class related to code manipulation. /// Provides methods and properties related to code manipulation. /// -public abstract class BaseProcessMemoryCodeExtensionTest : ProcessMemoryTest +public abstract class BaseProcessMemoryCodeExtensionTest : BaseProcessMemoryTest { /// /// Finds and returns the address of the MOV instruction that loads the new long value in the target app into the diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index e251820..83bacd8 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -8,7 +8,7 @@ namespace MindControl.Test.ProcessMemoryTests; /// Tests the features of the class related to memory allocation. /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryAllocationTest : ProcessMemoryTest +public class ProcessMemoryAllocationTest : BaseProcessMemoryTest { /// /// Tests the method. diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs index 95133ef..16e0501 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs @@ -6,7 +6,7 @@ namespace MindControl.Test.ProcessMemoryTests; /// Tests the features of the class related to attaching to a process. /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryAttachTest : ProcessMemoryTest +public class ProcessMemoryAttachTest : BaseProcessMemoryTest { /// /// This test only ensures that the setup works, i.e. that opening a process as a diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs index 4a78228..514c18e 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs @@ -9,7 +9,7 @@ namespace MindControl.Test.ProcessMemoryTests; /// Most pointer path evaluation features are implicitly tested through memory reading and writing methods. /// This test class focuses on special cases. [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryEvaluateTest : ProcessMemoryTest +public class ProcessMemoryEvaluateTest : BaseProcessMemoryTest { /// /// Tests the nominal case, with a path that evaluates to a valid address. diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs index cf5fa77..e951500 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs @@ -6,7 +6,7 @@ namespace MindControl.Test.ProcessMemoryTests; /// Tests the features of the class related to finding bytes in the target process. /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryFindBytesTest : ProcessMemoryTest +public class ProcessMemoryFindBytesTest : BaseProcessMemoryTest { /// /// Tests the method with a known fixed bytes pattern. diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs index dc83884..f14896d 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs @@ -7,9 +7,19 @@ namespace MindControl.Test.ProcessMemoryTests; /// Tests the features of the class related to injecting a library. /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryInjectionTest : ProcessMemoryTest +public class ProcessMemoryInjectionTest : BaseProcessMemoryTest { - private const string InjectedLibraryPath = "./MindControl.Test.InjectedLibrary.dll"; + /// + /// Gets a relative path to the injected library appropriate for the given bitness, or, by default, for + /// the bitness of the test. + /// + /// Whether to use the 64-bit version of the library. Default is the bitness of the + /// test. + protected string GetInjectedLibraryPath(bool? use64Bit = null) + { + use64Bit ??= Is64Bit; + return $"./InjectedLibrary/{(use64Bit.Value ? "x64" : "x86")}/MindControl.Test.InjectedLibrary.dll"; + } /// /// Ensures the tests are correctly set up before running. @@ -17,7 +27,7 @@ public class ProcessMemoryInjectionTest : ProcessMemoryTest [SetUp] public void SetUp() { - if (!File.Exists(InjectedLibraryPath)) + if (!File.Exists(GetInjectedLibraryPath())) { throw new FileNotFoundException("Injected library not found. Make sure the project \"MindControl.Test.InjectedLibrary\" was built before running the tests."); } @@ -31,7 +41,7 @@ public void SetUp() [Test] public void InjectLibraryTest() { - var result = TestProcessMemory!.InjectLibrary(InjectedLibraryPath); + var result = TestProcessMemory!.InjectLibrary(GetInjectedLibraryPath()); Assert.That(result.IsSuccess, Is.True); var output = ProceedToNextStep(); Assert.That(output, Is.EqualTo("Injected library attached")); @@ -45,8 +55,11 @@ public void InjectLibraryTest() [Test] public void InjectLibraryWithNonAsciiPathTest() { - const string targetPath = "憂 鬱.dll"; - File.Copy(InjectedLibraryPath, targetPath, true); + string injectedLibraryPath = GetInjectedLibraryPath(); + const string targetFileName = "憂 鬱.dll"; + string targetPath = Path.Combine(Path.GetDirectoryName(injectedLibraryPath)!, targetFileName); + + File.Copy(GetInjectedLibraryPath(), targetPath, true); var result = TestProcessMemory!.InjectLibrary(targetPath); Assert.That(result.IsSuccess, Is.True); var output = ProceedToNextStep(); diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index 9475439..835ca24 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -8,7 +8,7 @@ namespace MindControl.Test.ProcessMemoryTests; /// Tests the memory reading methods of . /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryReadTest : ProcessMemoryTest +public class ProcessMemoryReadTest : BaseProcessMemoryTest { #region Bytes reading diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs index b73c7a4..0827770 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs @@ -6,34 +6,34 @@ namespace MindControl.Test.ProcessMemoryTests; /// Tests the memory writing methods of . /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryWriteTest : ProcessMemoryTest +public class ProcessMemoryWriteTest : BaseProcessMemoryTest { /// /// Write a value in the target app memory, and assert that the output of the app for that specific value (and no /// other value) reflects the change. /// - /// Pointer path fragment to insert after the address of the target object instance - /// in order to reach the address where we want to write the value. /// Value to write. /// Index of the value to assert in the array representing the final output from the /// target app. /// Expected value of the final output from the target app. - [TestCase("+48", true, 0, "True")] - [TestCase("+49", (byte)94, 1, "94")] - [TestCase("+38", (int)-447712345, 2, "-447712345")] - [TestCase("+3C", (uint)74753312, 3, "74753312")] - [TestCase("+20", (long)-858884523, 5, "-858884523")] - [TestCase("+28", (ulong)755443121891, 6, "755443121891")] - [TestCase("+10,8", (long)51356, 9, "51356")] - [TestCase("+44", (short)-2421, 10, "-2421")] - [TestCase("+46", (ushort)2594, 11, "2594")] - [TestCase("+40", (float)4474.783, 12, "4474.783")] - [TestCase("+30", (double)54234423.3147, 13, "54234423.3147")] - [TestCase("+18,10", new byte[] { 0x8, 0x6, 0x4, 0xA }, 14, "8,6,4,10")] - public void WriteValueTest(string pointerPathSuffix, object value, int finalResultsIndex, string expectedValue) + [TestCase(true, IndexOfOutputOuterBool, "True")] + [TestCase((byte)94, IndexOfOutputOuterByte, "94")] + [TestCase((int)-447712345, IndexOfOutputOuterInt, "-447712345")] + [TestCase((uint)74753312, IndexOfOutputOuterUint, "74753312")] + [TestCase((long)-858884523, IndexOfOutputOuterLong, "-858884523")] + [TestCase((ulong)755443121891, IndexOfOutputOuterUlong, "755443121891")] + [TestCase((long)51356, IndexOfOutputInnerLong, "51356")] + [TestCase((short)-2421, IndexOfOutputOuterShort, "-2421")] + [TestCase((ushort)2594, IndexOfOutputOuterUshort, "2594")] + [TestCase((float)4474.783, IndexOfOutputOuterFloat, "4474.783")] + [TestCase((double)54234423.3147, IndexOfOutputOuterDouble, "54234423.3147")] + [TestCase(new byte[] { 0x8, 0x6, 0x4, 0xA }, IndexOfOutputOuterByteArray, "8,6,4,10")] + public void WriteValueTest(object value, int finalResultsIndex, string expectedValue) { + var result = TestProcessMemory.Read(OuterClassPointer + (UIntPtr)0x10).Value; + var pointerPath = GetPointerPathForValueAtIndex(finalResultsIndex); ProceedToNextStep(); - TestProcessMemory!.Write($"{OuterClassPointer:X}{pointerPathSuffix}", value); + TestProcessMemory!.Write(pointerPath, value); ProceedToNextStep(); AssertFinalResults(finalResultsIndex, expectedValue); } @@ -69,17 +69,23 @@ public void WriteStructTest() [Test] public void WriteStringPointerTest() { - var pointerAddress = OuterClassPointer + 8; + var pointerAddress = GetPointerPathForValueAtIndex(IndexOfOutputOuterString); var stringSettings = TestProcessMemory!.FindStringSettings(pointerAddress, "ThisIsÄString").Value; ProceedToNextStep(); // Make sure we make the test program change the string pointer before we overwrite it - var newString = "ThisStringIsCompletelyOriginalAndAlsoLongerThanTheOriginalOne"; + var newString = "This String Is Completely New And Also Longer Than The Original One"; var newStringReservation = TestProcessMemory.StoreString(newString, stringSettings).Value; TestProcessMemory.Write(pointerAddress, newStringReservation.Address); ProceedToNextStep(); // Makes the test program output the final results // Test that the program actually used (wrote to the console) the string that we hacked in - Assert.That(FinalResults[4], Is.EqualTo(newString)); + AssertFinalResults(IndexOfOutputOuterString, newString); } -} \ No newline at end of file +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryWriteTestX86 : ProcessMemoryWriteTest { protected override bool Is64Bit => false; } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessTrackerTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessTrackerTest.cs index 9347102..82b9088 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessTrackerTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessTrackerTest.cs @@ -48,7 +48,7 @@ public void CleanUp() /// private Process StartTargetAppProcess() { - var process = ProcessMemoryTest.StartTargetAppProcess(); + var process = BaseProcessMemoryTest.StartTargetAppProcess(); _targetProcesses.Add(process); Thread.Sleep(500); // Wait a bit to make sure the process is ready. return process; From ad2e8bade81a2cee66327b9a2f170ae1bbae4858 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 22 Jun 2024 16:03:16 +0200 Subject: [PATCH 29/66] x86 reading tests --- .../BaseProcessMemoryTest.cs | 174 ++++++++++--- .../ProcessMemoryAllocationTest.cs | 8 +- .../ProcessMemoryReadTest.cs | 229 +++++++++--------- .../ProcessMemoryWriteTest.cs | 31 ++- 4 files changed, 271 insertions(+), 171 deletions(-) diff --git a/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs b/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs index ac5f0ae..a201a26 100644 --- a/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs @@ -17,10 +17,11 @@ public class BaseProcessMemoryTest protected const string MainModuleName = "MindControl.Test.TargetApp.dll"; /// Settings that apply to strings used in our target .net process. - /// The type prefix is dynamic in reality. Here, we use a stub array with only 0, which is enough to serve - /// our purposes for the tests. - protected static readonly StringSettings DotNetStringSettings = new(Encoding.Unicode, true, - new StringLengthPrefix(4, StringLengthUnit.Characters), new byte[8]); + /// The type prefix is dynamic in reality. Here, we use a stub array with the right size that only holds + /// zeroes, which is enough to serve our purposes for the tests. + protected StringSettings GetDotNetStringSettings() + => new(Encoding.Unicode, true, new StringLengthPrefix(4, StringLengthUnit.Characters), + new byte[Is64Bit ? 8 : 4]); private Process? _targetProcess; protected ProcessMemory? TestProcessMemory; @@ -113,6 +114,68 @@ protected void ProceedUntilProcessEnds() ProceedToNextStep(); ProceedToNextStep(); } + + /// Initial value of the outer class instance bool value. + protected const bool InitialBoolValue = true; + /// Initial value of the outer class instance byte value. + protected const byte InitialByteValue = 0xAC; + /// Initial value of the outer class instance int value. + protected const int InitialIntValue = -7651; + /// Initial value of the outer class instance uint value. + protected const uint InitialUIntValue = 6781631; + /// Initial value of the outer class instance string value. + protected const string InitialStringValue = "ThisIsÄString"; + /// Initial value of the outer class instance long value. + protected const long InitialLongValue = -65746876815103L; + /// Initial value of the outer class instance ulong value. + protected const ulong InitialULongValue = 76354111324644L; + /// Initial value of the inner class instance byte value. + protected const byte InitialInnerByteValue = 0xAA; + /// Initial value of the inner class instance int value. + protected const int InitialInnerIntValue = 1111111; + /// Initial value of the inner class instance long value. + protected const long InitialInnerLongValue = 999999999999L; + /// Initial value of the outer class instance short value. + protected const short InitialShortValue = -7777; + /// Initial value of the outer class instance ushort value. + protected const ushort InitialUShortValue = 8888; + /// Initial value of the outer class instance float value. + protected const float InitialFloatValue = 3456765.323f; + /// Initial value of the outer class instance double value. + protected const double InitialDoubleValue = 79879131651.333454; + /// Initial value of the outer class instance byte array value. + protected static readonly byte[] InitialByteArrayValue = [0x11, 0x22, 0x33, 0x44]; + + /// Expected final value of the outer class instance bool value. + protected const bool ExpectedFinalBoolValue = false; + /// Expected final value of the outer class instance byte value. + protected const byte ExpectedFinalByteValue = 0xDC; + /// Expected final value of the outer class instance int value. + protected const int ExpectedFinalIntValue = 987411; + /// Expected final value of the outer class instance uint value. + protected const uint ExpectedFinalUIntValue = 444763; + /// Expected final value of the outer class instance string value. + protected const string ExpectedFinalStringValue = "ThisIsALongerStrîngWith文字化けチェック"; + /// Expected final value of the outer class instance long value. + protected const long ExpectedFinalLongValue = -777654646516513; + /// Expected final value of the outer class instance ulong value. + protected const ulong ExpectedFinalULongValue = 34411111111164; + /// Expected final value of the inner class instance byte value. + protected const byte ExpectedFinalInnerByteValue = 0xAD; + /// Expected final value of the inner class instance int value. + protected const int ExpectedFinalInnerIntValue = 64646321; + /// Expected final value of the inner class instance long value. + protected const long ExpectedFinalInnerLongValue = 7777777777777; + /// Expected final value of the outer class instance short value. + protected const short ExpectedFinalShortValue = -8888; + /// Expected final value of the outer class instance ushort value. + protected const ushort ExpectedFinalUShortValue = 9999; + /// Expected final value of the outer class instance float value. + protected const float ExpectedFinalFloatValue = -123444.15f; + /// Expected final value of the outer class instance double value. + protected const double ExpectedFinalDoubleValue = -99879416311.4478; + /// Expected final value of the outer class instance byte array value. + protected static readonly byte[] ExpectedFinalByteArrayValue = [0x55, 0x66, 0x77, 0x88]; /// /// Stores the line-by-line expected output of the target app. @@ -137,19 +200,19 @@ protected void ProceedUntilProcessEnds() ]; /// Index, in the target app output, of the outer class instance bool value. - protected const int IndexOfOutputOuterBool = 0; + protected const int IndexOfOutputBool = 0; /// Index, in the target app output, of the outer class instance byte value. - protected const int IndexOfOutputOuterByte = 1; + protected const int IndexOfOutputByte = 1; /// Index, in the target app output, of the outer class instance int value. - protected const int IndexOfOutputOuterInt = 2; + protected const int IndexOfOutputInt = 2; /// Index, in the target app output, of the outer class instance uint value. - protected const int IndexOfOutputOuterUint = 3; + protected const int IndexOfOutputUInt = 3; /// Index, in the target app output, of the outer class instance string value. - protected const int IndexOfOutputOuterString = 4; + protected const int IndexOfOutputString = 4; /// Index, in the target app output, of the outer class instance long value. - protected const int IndexOfOutputOuterLong = 5; + protected const int IndexOfOutputLong = 5; /// Index, in the target app output, of the outer class instance ulong value. - protected const int IndexOfOutputOuterUlong = 6; + protected const int IndexOfOutputULong = 6; /// Index, in the target app output, of the inner class instance byte value. protected const int IndexOfOutputInnerByte = 7; /// Index, in the target app output, of the inner class instance int value. @@ -157,41 +220,92 @@ protected void ProceedUntilProcessEnds() /// Index, in the target app output, of the inner class instance long value. protected const int IndexOfOutputInnerLong = 9; /// Index, in the target app output, of the outer class instance short value. - protected const int IndexOfOutputOuterShort = 10; + protected const int IndexOfOutputShort = 10; /// Index, in the target app output, of the outer class instance ushort value. - protected const int IndexOfOutputOuterUshort = 11; + protected const int IndexOfOutputUShort = 11; /// Index, in the target app output, of the outer class instance float value. - protected const int IndexOfOutputOuterFloat = 12; + protected const int IndexOfOutputFloat = 12; /// Index, in the target app output, of the outer class instance double value. - protected const int IndexOfOutputOuterDouble = 13; + protected const int IndexOfOutputDouble = 13; /// Index, in the target app output, of the outer class instance byte array value. - protected const int IndexOfOutputOuterByteArray = 14; + protected const int IndexOfOutputByteArray = 14; /// - /// Gets the pointer path for the value at the specified index by order of output of the target app. + /// Gets the pointer path for the value at the specified index by order of output of the target app, regardless of + /// its bitness. /// /// Index of the value to get the pointer path for, by order of output of the target app. /// /// Pointer path to the value at the specified index. - /// Thrown if the index is not recognized. + /// Thrown if the index is not recognized or invalid. protected PointerPath GetPointerPathForValueAtIndex(int index) => OuterClassPointer.ToString("X") + index switch { - IndexOfOutputOuterBool => Is64Bit ? "+48" : "+38", - IndexOfOutputOuterByte => Is64Bit ? "+49" : "+39", - IndexOfOutputOuterInt => Is64Bit ? "+38" : "+28", - IndexOfOutputOuterUint => Is64Bit ? "+3C" : "+2C", - IndexOfOutputOuterString => Is64Bit ? "+8" : "+1C", - IndexOfOutputOuterLong => Is64Bit ? "+20" : "+4", - IndexOfOutputOuterUlong => Is64Bit ? "+28" : "+C", + IndexOfOutputBool => Is64Bit ? "+48" : "+38", + IndexOfOutputByte => Is64Bit ? "+49" : "+39", + IndexOfOutputInt => Is64Bit ? "+38" : "+28", + IndexOfOutputUInt => Is64Bit ? "+3C" : "+2C", + IndexOfOutputString => Is64Bit ? "+8" : "+1C", + IndexOfOutputLong => Is64Bit ? "+20" : "+4", + IndexOfOutputULong => Is64Bit ? "+28" : "+C", IndexOfOutputInnerLong => Is64Bit ? "+10,8" : "+20,4", - IndexOfOutputOuterShort => Is64Bit ? "+44" : "+34", - IndexOfOutputOuterUshort => Is64Bit ? "+46" : "+36", - IndexOfOutputOuterFloat => Is64Bit ? "+40" : "+30", - IndexOfOutputOuterDouble => Is64Bit ? "+30" : "+14", - IndexOfOutputOuterByteArray => Is64Bit ? "+18,10" : "+24,8", + IndexOfOutputShort => Is64Bit ? "+44" : "+34", + IndexOfOutputUShort => Is64Bit ? "+46" : "+36", + IndexOfOutputFloat => Is64Bit ? "+40" : "+30", + IndexOfOutputDouble => Is64Bit ? "+30" : "+14", + IndexOfOutputByteArray => Is64Bit ? "+18,10" : "+24,8", _ => throw new ArgumentOutOfRangeException(nameof(index)) }; + /// + /// Gets the address to the value at the specified index by order of output of the target app, regardless of its + /// bitness. + /// + /// Index of the target value, by order of output of the target app. + /// Address of the value at the specified index. + /// Thrown if the index is not recognized or invalid. + protected UIntPtr GetAddressForValueAtIndex(int index) => OuterClassPointer + (UIntPtr)(index switch + { + IndexOfOutputBool => Is64Bit ? 0x48 : 0x38, + IndexOfOutputByte => Is64Bit ? 0x49 : 0x39, + IndexOfOutputInt => Is64Bit ? 0x38 : 0x28, + IndexOfOutputUInt => Is64Bit ? 0x3C : 0x2C, + IndexOfOutputString => Is64Bit ? 0x8 : 0x1C, + IndexOfOutputLong => Is64Bit ? 0x20 : 0x4, + IndexOfOutputULong => Is64Bit ? 0x28 : 0xC, + IndexOfOutputShort => Is64Bit ? 0x44 : 0x34, + IndexOfOutputUShort => Is64Bit ? 0x46 : 0x36, + IndexOfOutputFloat => Is64Bit ? 0x40 : 0x30, + IndexOfOutputDouble => Is64Bit ? 0x30 : 0x14, + _ => throw new ArgumentOutOfRangeException(nameof(index)) + }); + + /// + /// Gets the pointer path to the raw bytes of the output string of the target app, regardless of its bitness. + /// + protected PointerPath GetPathToRawStringBytes() + => OuterClassPointer.ToString("X") + (Is64Bit ? "+8,C" : "+1C,8"); + + /// + /// Gets a pointer path that evaluates to an address pointing to a 0xFFFFFFFFFFFFFFFF (x64) or 0xFFFFFFFF (x86) + /// value. Warning: the path itself does not evaluate to the max address, but to a pointer to it. To clarify with + /// an example, this path will evaluate to say 0x4A64F850, and if you read a ulong at that address, you will get + /// 0xFFFFFFFFFFFFFFFF. + /// + protected PointerPath GetPathToPointerToMaxAddress() + => OuterClassPointer.ToString("X") + (Is64Bit ? "+10,10" : "+20,10"); + + /// + /// Gets the address of the pointer of the output string of the target app, regardless of its bitness. + /// + protected UIntPtr GetStringPointerAddress() + => OuterClassPointer + (Is64Bit ? (UIntPtr)0x8 : 0x1C); + + /// + /// Gets the maximum value that a pointer can have in the target app (which depends of its bitness). + /// + protected UIntPtr GetMaxPointerValue() + => Is64Bit ? UIntPtr.MaxValue : uint.MaxValue; + /// /// Asserts that among the final results output by the target app, the one at the given index matches the /// expected value, and all the other results are the known, untouched values. diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index 83bacd8..80d2795 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -364,12 +364,12 @@ public void StoreWithMultipleOverflowingValuesTest() public void StoreStringWithoutAllocationTest() { var stringToStore = "Hello 世界!"; - var reservationResult = TestProcessMemory!.StoreString(stringToStore, DotNetStringSettings); + var reservationResult = TestProcessMemory!.StoreString(stringToStore, GetDotNetStringSettings()); Assert.That(reservationResult.IsSuccess, Is.True); var reservation = reservationResult.Value; var bytesReadBack = TestProcessMemory.ReadBytes(reservation.Address, reservation.Range.GetSize()).Value; - var stringReadBack = DotNetStringSettings.GetString(bytesReadBack); + var stringReadBack = GetDotNetStringSettings().GetString(bytesReadBack); // The store method should have allocated a new range. Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); @@ -390,12 +390,12 @@ public void StoreStringWithAllocationTest() var stringToStore = "Hello 世界!"; var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; - var reservationResult = TestProcessMemory.StoreString(stringToStore, DotNetStringSettings, allocation); + var reservationResult = TestProcessMemory.StoreString(stringToStore, GetDotNetStringSettings(), allocation); Assert.That(reservationResult.IsSuccess, Is.True); var reservation = reservationResult.Value; var bytesReadBack = TestProcessMemory.ReadBytes(reservation.Address, reservation.Range.GetSize()).Value; - var stringReadBack = DotNetStringSettings.GetString(bytesReadBack); + var stringReadBack = GetDotNetStringSettings().GetString(bytesReadBack); Assert.That(reservation.IsDisposed, Is.False); // The resulting range should be a range reserved from our original range. diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index 835ca24..3ec3cfd 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -22,7 +22,7 @@ public class ProcessMemoryReadTest : BaseProcessMemoryTest [Test] public void ReadBytesTest() { - PointerPath pointerPath = $"{OuterClassPointer:X}+18,10"; + PointerPath pointerPath = GetPointerPathForValueAtIndex(IndexOfOutputByteArray); Assert.That(TestProcessMemory!.ReadBytes(pointerPath, 4).GetValueOrDefault(), Is.EqualTo(new byte[] { 0x11, 0x22, 0x33, 0x44 })); ProceedToNextStep(); @@ -49,7 +49,7 @@ public void ReadBytesWithZeroPointerTest() [Test] public void ReadBytesWithUnreadableAddressTest() { - var result = TestProcessMemory!.ReadBytes(UIntPtr.MaxValue, 1); + var result = TestProcessMemory!.ReadBytes(GetMaxPointerValue(), 1); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); var systemError = ((ReadFailureOnSystemRead)result.Error).Details; @@ -63,7 +63,7 @@ public void ReadBytesWithUnreadableAddressTest() [Test] public void ReadBytesWithInvalidPathTest() { - var result = TestProcessMemory!.ReadBytes($"{OuterClassPointer:X}+10,10,0,0", 4); + var result = TestProcessMemory!.ReadBytes("bad pointer path", 4); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); var pathError = ((ReadFailureOnPointerPathEvaluation)result.Error).Details; @@ -126,7 +126,7 @@ public void ReadBytesOnPartiallyUnreadableRangeTest() [Test] public void ReadBytesPartialTest() { - PointerPath pointerPath = $"{OuterClassPointer:X}+18,10"; + PointerPath pointerPath = GetPointerPathForValueAtIndex(IndexOfOutputByteArray); var firstBuffer = new byte[4]; var firstResult = TestProcessMemory!.ReadBytesPartial(pointerPath, firstBuffer, 4); ProceedToNextStep(); @@ -161,7 +161,7 @@ public void ReadBytesPartialWithZeroPointerTest() [Test] public void ReadBytesPartialWithUnreadableAddressTest() { - var result = TestProcessMemory!.ReadBytesPartial(UIntPtr.MaxValue, new byte[1], 1); + var result = TestProcessMemory!.ReadBytesPartial(GetMaxPointerValue(), new byte[1], 1); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); var systemError = ((ReadFailureOnSystemRead)result.Error).Details; @@ -175,7 +175,7 @@ public void ReadBytesPartialWithUnreadableAddressTest() [Test] public void ReadBytesPartialWithInvalidPathTest() { - var result = TestProcessMemory!.ReadBytesPartial($"{OuterClassPointer:X}+10,10,0,0", new byte[4], 4); + var result = TestProcessMemory!.ReadBytesPartial("bad pointer path", new byte[4], 4); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); var pathError = ((ReadFailureOnPointerPathEvaluation)result.Error).Details; @@ -189,7 +189,7 @@ public void ReadBytesPartialWithInvalidPathTest() [Test] public void ReadBytesPartialWithZeroLengthTest() { - var result = TestProcessMemory!.ReadBytesPartial(OuterClassPointer, Array.Empty(), 0); + var result = TestProcessMemory!.ReadBytesPartial(OuterClassPointer, [], 0); Assert.That(result.IsSuccess, Is.True); Assert.That(result.Value, Is.Zero); } @@ -244,24 +244,23 @@ public void ReadBytesPartialOnPartiallyUnreadableRangeTest() /// The values are expected to match the specified ones. /// /// Type of data to read. - /// Offset to add up with the outer class pointer to get the address of the value to - /// read. + /// Index of the value to read, by order of output in the target app. /// Expected value on the first read (after initialization). /// Expected value on the second read (after modification). - [TestCase(typeof(bool), 0x48, true, false)] - [TestCase(typeof(byte), 0x49, 0xAC, 0xDC)] - [TestCase(typeof(short), 0x44, -7777, -8888)] - [TestCase(typeof(ushort), 0x46, 8888, 9999)] - [TestCase(typeof(int), 0x38, -7651, 987411)] - [TestCase(typeof(uint), 0x3C, 6781631, 444763)] - [TestCase(typeof(long), 0x20, -65746876815103L, -777654646516513L)] - [TestCase(typeof(ulong), 0x28, 76354111324644L, 34411111111164L)] - [TestCase(typeof(float), 0x40, 3456765.323f, -123444.147f)] - [TestCase(typeof(double), 0x30, 79879131651.33345, -99879416311.4478)] - public void ReadTwoStepGenericTest(Type targetType, int pointerOffset, object? expectedResultBeforeBreak, + [TestCase(typeof(bool), IndexOfOutputBool, InitialBoolValue, ExpectedFinalBoolValue)] + [TestCase(typeof(byte), IndexOfOutputByte, InitialByteValue, ExpectedFinalByteValue)] + [TestCase(typeof(short), IndexOfOutputShort, InitialShortValue, ExpectedFinalShortValue)] + [TestCase(typeof(ushort), IndexOfOutputUShort, InitialUShortValue, ExpectedFinalUShortValue)] + [TestCase(typeof(int), IndexOfOutputInt, InitialIntValue, ExpectedFinalIntValue)] + [TestCase(typeof(uint), IndexOfOutputUInt, InitialUIntValue, ExpectedFinalUIntValue)] + [TestCase(typeof(long), IndexOfOutputLong, InitialLongValue, ExpectedFinalLongValue)] + [TestCase(typeof(ulong), IndexOfOutputULong, InitialULongValue, ExpectedFinalULongValue)] + [TestCase(typeof(float), IndexOfOutputFloat, InitialFloatValue, ExpectedFinalFloatValue)] + [TestCase(typeof(double), IndexOfOutputDouble, InitialDoubleValue, ExpectedFinalDoubleValue)] + public void ReadTwoStepGenericTest(Type targetType, int valueIndexInOutput, object? expectedResultBeforeBreak, object? expectedResultAfterBreak) { - UIntPtr targetIntAddress = OuterClassPointer + (UIntPtr)pointerOffset; + UIntPtr targetIntAddress = GetAddressForValueAtIndex(valueIndexInOutput); object resultBefore = TestProcessMemory!.Read(targetType, targetIntAddress).Value; Assert.That(resultBefore, Is.EqualTo(expectedResultBeforeBreak)); ProceedToNextStep(); @@ -277,10 +276,10 @@ public void ReadTwoStepGenericTest(Type targetType, int pointerOffset, object? e [Test] public void ReadBoolTest() { - var address = OuterClassPointer + 0x48; - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(true)); + var address = GetAddressForValueAtIndex(IndexOfOutputBool); + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialBoolValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(false)); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalBoolValue)); } /// @@ -291,10 +290,10 @@ public void ReadBoolTest() [Test] public void ReadByteTest() { - var address = OuterClassPointer + 0x49; - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(0xAC)); + var address = GetAddressForValueAtIndex(IndexOfOutputByte); + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialByteValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(0xDC)); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalByteValue)); } /// @@ -305,10 +304,10 @@ public void ReadByteTest() [Test] public void ReadShortTest() { - var address = OuterClassPointer + 0x44; - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(-7777)); + var address = GetAddressForValueAtIndex(IndexOfOutputShort); + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialShortValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(-8888)); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalShortValue)); } /// @@ -319,10 +318,10 @@ public void ReadShortTest() [Test] public void ReadUShortTest() { - var address = OuterClassPointer + 0x46; - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(8888)); + var address = GetAddressForValueAtIndex(IndexOfOutputUShort); + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialUShortValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(9999)); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalUShortValue)); } /// @@ -333,10 +332,10 @@ public void ReadUShortTest() [Test] public void ReadIntTest() { - var address = OuterClassPointer + 0x38; - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(-7651)); + var address = GetAddressForValueAtIndex(IndexOfOutputInt); + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialIntValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(987411)); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalIntValue)); } /// @@ -347,10 +346,10 @@ public void ReadIntTest() [Test] public void ReadUIntTest() { - var address = OuterClassPointer + 0x3C; - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(6781631)); + var address = GetAddressForValueAtIndex(IndexOfOutputUInt); + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialUIntValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(444763)); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalUIntValue)); } /// @@ -361,10 +360,10 @@ public void ReadUIntTest() [Test] public void ReadLongTest() { - var address = OuterClassPointer + 0x20; - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(-65746876815103L)); + var address = GetAddressForValueAtIndex(IndexOfOutputLong); + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialLongValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(-777654646516513L)); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalLongValue)); } /// @@ -375,10 +374,10 @@ public void ReadLongTest() [Test] public void ReadULongTest() { - var address = OuterClassPointer + 0x28; - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(76354111324644L)); + var address = GetAddressForValueAtIndex(IndexOfOutputULong); + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialULongValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(34411111111164L)); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalULongValue)); } /// @@ -389,10 +388,10 @@ public void ReadULongTest() [Test] public void ReadFloatTest() { - var address = OuterClassPointer + 0x40; - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(3456765.323f)); + var address = GetAddressForValueAtIndex(IndexOfOutputFloat); + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialFloatValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(-123444.147f)); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalFloatValue)); } /// @@ -403,10 +402,10 @@ public void ReadFloatTest() [Test] public void ReadDoubleTest() { - var address = OuterClassPointer + 0x30; - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(79879131651.33345)); + var address = GetAddressForValueAtIndex(IndexOfOutputDouble); + Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialDoubleValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(-99879416311.4478)); + Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalDoubleValue)); } /// @@ -417,7 +416,8 @@ public void ReadDoubleTest() /// [Test] public void ReadNestedLongTest() => Assert.That( - TestProcessMemory!.Read($"{OuterClassPointer:X}+10,8").GetValueOrDefault(), Is.EqualTo(999999999999L)); + TestProcessMemory!.Read(GetPointerPathForValueAtIndex(IndexOfOutputInnerLong)).GetValueOrDefault(), + Is.EqualTo(InitialInnerLongValue)); /// /// Tests . @@ -429,8 +429,8 @@ public void ReadNestedLongTest() => Assert.That( [Test] public void ReadUIntPtrMaxValueTest() { - var ptr = TestProcessMemory!.Read($"{OuterClassPointer:X}+10,10").GetValueOrDefault(); - Assert.That(ptr.ToUInt64(), Is.EqualTo(ulong.MaxValue)); + var ptr = TestProcessMemory!.Read(GetPathToPointerToMaxAddress()).GetValueOrDefault(); + Assert.That(ptr, Is.EqualTo(GetMaxPointerValue())); } /// @@ -442,7 +442,7 @@ public void ReadUIntPtrMaxValueTest() [Test] public void ReadAtMaxPointerValueTest() { - var result = TestProcessMemory!.Read($"{OuterClassPointer:X}+10,10,0"); + var result = TestProcessMemory!.Read(GetMaxPointerValue()); Assert.That(result.IsSuccess, Is.False); var error = result.Error; Assert.That(error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); @@ -452,25 +452,6 @@ public void ReadAtMaxPointerValueTest() Assert.That(osError.ErrorCode, Is.GreaterThan(0)); Assert.That(osError.ErrorMessage, Is.Not.Empty); } - - /// - /// Tests an edge case where the pointer path given to a read operation points to a value located after the last - /// possible byte in memory (the maximum value of a UIntPtr + 1). - /// The read operation is expected to fail with a . - /// - [Test] - public void ReadOverMaxPointerValueTest() - { - var result = TestProcessMemory!.Read($"{OuterClassPointer:X}+10,10,1"); - Assert.That(result.IsSuccess, Is.False); - var error = result.Error; - Assert.That(error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); - var pathError = ((ReadFailureOnPointerPathEvaluation)error).Details; - Assert.That(pathError, Is.TypeOf(typeof(PathEvaluationFailureOnPointerOutOfRange))); - var outOfRangeError = (PathEvaluationFailureOnPointerOutOfRange)pathError; - Assert.That(outOfRangeError.Offset, Is.EqualTo(new PointerOffset(1, false))); - Assert.That(outOfRangeError.PreviousAddress, Is.EqualTo(UIntPtr.MaxValue)); - } /// /// Tests the method with a reference type. @@ -489,7 +470,7 @@ public void ReadIncompatibleTypeTest() #region Structure reading /// A struct with a couple fields, to test reading structs. - public record struct TestStruct(double A, int B); + public record struct TestStruct(long A, ulong B); /// Structure used to test reading a structure with references. private record struct TestStructWithReferences(int A, string B); @@ -502,12 +483,12 @@ private record struct TestStructWithReferences(int A, string B); [Test] public void ReadStructureTest() { - var pointerPath = new PointerPath($"{OuterClassPointer:X}+30"); + var pointerPath = GetPointerPathForValueAtIndex(IndexOfOutputLong); Assert.That(TestProcessMemory!.Read(pointerPath).Value, - Is.EqualTo(new TestStruct(79879131651.33345, -7651))); + Is.EqualTo(new TestStruct(InitialLongValue, InitialULongValue))); ProceedToNextStep(); Assert.That(TestProcessMemory.Read(pointerPath).Value, - Is.EqualTo(new TestStruct(-99879416311.4478, 987411))); + Is.EqualTo(new TestStruct(ExpectedFinalLongValue, ExpectedFinalULongValue))); } /// @@ -518,12 +499,12 @@ public void ReadStructureTest() [Test] public void ReadStructureAsObjectTest() { - var pointerPath = new PointerPath($"{OuterClassPointer:X}+30"); + var pointerPath = GetPointerPathForValueAtIndex(IndexOfOutputLong); Assert.That(TestProcessMemory!.Read(typeof(TestStruct), pointerPath).Value, - Is.EqualTo(new TestStruct(79879131651.33345, -7651))); + Is.EqualTo(new TestStruct(InitialLongValue, InitialULongValue))); ProceedToNextStep(); Assert.That(TestProcessMemory.Read(typeof(TestStruct), pointerPath).Value, - Is.EqualTo(new TestStruct(-99879416311.4478, 987411))); + Is.EqualTo(new TestStruct(ExpectedFinalLongValue, ExpectedFinalULongValue))); } /// @@ -547,7 +528,7 @@ public void ReadStructureWithReferencesTest() public record StringSettingsTestCase(Encoding Encoding, string String, bool IsNullTerminated, StringLengthPrefix? LengthPrefix, byte[]? TypePrefix); - private static readonly byte[] ExampleTypePrefix = { 0x11, 0x22, 0x33, 0x44 }; + private static readonly byte[] ExampleTypePrefix = [0x11, 0x22, 0x33, 0x44]; private static readonly StringSettingsTestCase[] StringSettingsTestCases = { new(Encoding.Unicode, "Simple string", true, null, null), @@ -624,16 +605,17 @@ public void FindStringSettingsTest(StringSettingsTestCase testCase) [Test] public void FindStringSettingsOnKnownPathTest() { - var pointerPath = new PointerPath($"{OuterClassPointer:X}+8"); - var result = TestProcessMemory!.FindStringSettings(pointerPath, "ThisIsÄString"); + var dotNetStringSettings = GetDotNetStringSettings(); + var result = TestProcessMemory!.FindStringSettings(GetPointerPathForValueAtIndex(IndexOfOutputString), + InitialStringValue); Assert.That(result.IsSuccess, Is.True); - Assert.That(result.Value.Encoding, Is.EqualTo(DotNetStringSettings.Encoding)); - Assert.That(result.Value.IsNullTerminated, Is.EqualTo(DotNetStringSettings.IsNullTerminated)); + Assert.That(result.Value.Encoding, Is.EqualTo(dotNetStringSettings.Encoding)); + Assert.That(result.Value.IsNullTerminated, Is.EqualTo(dotNetStringSettings.IsNullTerminated)); Assert.That(result.Value.LengthPrefix?.Size, - Is.EqualTo(DotNetStringSettings.LengthPrefix?.Size)); - Assert.That(result.Value.LengthPrefix?.Unit, Is.EqualTo(DotNetStringSettings.LengthPrefix?.Unit)); + Is.EqualTo(dotNetStringSettings.LengthPrefix?.Size)); + Assert.That(result.Value.LengthPrefix?.Unit, Is.EqualTo(dotNetStringSettings.LengthPrefix?.Unit)); // For the type prefix, we only check the length, because the actual value is dynamic. - Assert.That(result.Value.TypePrefix?.Length, Is.EqualTo(DotNetStringSettings.TypePrefix?.Length)); + Assert.That(result.Value.TypePrefix?.Length, Is.EqualTo(dotNetStringSettings.TypePrefix?.Length)); } /// @@ -644,7 +626,7 @@ public void FindStringSettingsOnKnownPathTest() [Test] public void FindStringSettingsWithWrongStringTest() { - var result = TestProcessMemory!.FindStringSettings(OuterClassPointer+8, "Wrong string"); + var result = TestProcessMemory!.FindStringSettings(GetStringPointerAddress(), "Wrong string"); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnNoSettingsFound))); } @@ -657,7 +639,7 @@ public void FindStringSettingsWithWrongStringTest() [Test] public void FindStringSettingsWithEmptyStringTest() { - var result = TestProcessMemory!.FindStringSettings(OuterClassPointer+8, ""); + var result = TestProcessMemory!.FindStringSettings(GetStringPointerAddress(), ""); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnNoSettingsFound))); } @@ -684,7 +666,7 @@ public void FindStringSettingsOnZeroPointerTest() [Test] public void FindStringSettingsOnInvalidPointerAddressTest() { - var result = TestProcessMemory!.FindStringSettings(UIntPtr.MaxValue, "Whatever"); + var result = TestProcessMemory!.FindStringSettings(GetMaxPointerValue(), "Whatever"); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnPointerReadFailure))); var failure = (FindStringSettingsFailureOnPointerReadFailure)result.Error; @@ -699,8 +681,8 @@ public void FindStringSettingsOnInvalidPointerAddressTest() [Test] public void FindStringSettingsOnInvalidStringAddressTest() { - // Use a known path that should cause the string address to be 0xFFFFFFFFFFFFFFFF. - PointerPath pointerPath = $"{OuterClassPointer:X}+10,10"; + // Use a known path that should cause the string address to be 0xFFFFFFFFFFFFFFFF (x64) or 0xFFFFFFFF (x86). + PointerPath pointerPath = GetPathToPointerToMaxAddress(); var result = TestProcessMemory!.FindStringSettings(pointerPath, "Whatever"); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnStringReadFailure))); @@ -709,17 +691,14 @@ public void FindStringSettingsOnInvalidStringAddressTest() } /// - /// Tests with a pointer path that traverses - /// invalid addresses. + /// Tests with a pointer path that cannot be + /// evaluated. /// The method should return a . /// [Test] public void FindStringSettingsOnInvalidPointerPathTest() { - // Use a known pointer path that has a value of 0xFFFFFFFFFFFFFFFF, and add a ",1", which should cause a - // failure in the pointer path evaluation. - PointerPath pointerPath = $"{OuterClassPointer:X}+10,10,1"; - var result = TestProcessMemory!.FindStringSettings(pointerPath, "Whatever"); + var result = TestProcessMemory!.FindStringSettings("bad pointer path", "Whatever"); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnPointerPathEvaluation))); var failure = (FindStringSettingsFailureOnPointerPathEvaluation)result.Error; @@ -778,16 +757,16 @@ public void ReadRawStringTest(ReadRawStringTestCase testCase) [Test] public void ReadRawStringWithKnownStringTest() { - var path = $"{OuterClassPointer:X}+8,C"; + var path = GetPathToRawStringBytes(); var firstResult = TestProcessMemory!.ReadRawString(path, Encoding.Unicode); ProceedToNextStep(); var secondResult = TestProcessMemory.ReadRawString(path, Encoding.Unicode); Assert.That(firstResult.IsSuccess, Is.True); - Assert.That(firstResult.Value, Is.EqualTo("ThisIsÄString")); + Assert.That(firstResult.Value, Is.EqualTo(InitialStringValue)); Assert.That(secondResult.IsSuccess, Is.True); - Assert.That(secondResult.Value, Is.EqualTo("ThisIsALongerStrîngWith文字化けチェック")); + Assert.That(secondResult.Value, Is.EqualTo(ExpectedFinalStringValue)); } /// @@ -811,7 +790,7 @@ public void ReadRawStringWithZeroPointerTest() [Test] public void ReadRawStringWithUnreadableAddressTest() { - var result = TestProcessMemory!.ReadRawString(UIntPtr.MaxValue, Encoding.Unicode); + var result = TestProcessMemory!.ReadRawString(GetMaxPointerValue(), Encoding.Unicode); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); var systemError = ((ReadFailureOnSystemRead)result.Error).Details; @@ -826,20 +805,20 @@ public void ReadRawStringWithUnreadableAddressTest() [Test] public void ReadRawStringWithNegativeMaxLengthTest() { - var result = TestProcessMemory!.ReadRawString($"{OuterClassPointer:X}+8,C", Encoding.Unicode, -1); + var result = TestProcessMemory!.ReadRawString(GetPathToRawStringBytes(), Encoding.Unicode, -1); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnInvalidArguments))); } /// /// Tests . - /// Call the method with a known path that cannot be evaluated. + /// Call the method with a pointer path that cannot be evaluated. /// Expect the result to be a . /// [Test] public void ReadRawStringWithBadPointerPathTest() { - var result = TestProcessMemory!.ReadRawString($"{OuterClassPointer:X}+10,10,0,0", Encoding.Unicode); + var result = TestProcessMemory!.ReadRawString("bad pointer path", Encoding.Unicode); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); var pathError = ((ReadFailureOnPointerPathEvaluation)result.Error).Details; @@ -898,16 +877,16 @@ public void ReadStringPointerTest(StringSettingsTestCase testCase) [Test] public void ReadStringPointerOnKnownStringTest() { - var path = $"{OuterClassPointer:X}+8"; - var firstResult = TestProcessMemory!.ReadStringPointer(path, DotNetStringSettings); + var path = GetPointerPathForValueAtIndex(IndexOfOutputString); + var firstResult = TestProcessMemory!.ReadStringPointer(path, GetDotNetStringSettings()); ProceedToNextStep(); - var secondResult = TestProcessMemory.ReadStringPointer(path, DotNetStringSettings); + var secondResult = TestProcessMemory.ReadStringPointer(path, GetDotNetStringSettings()); Assert.That(firstResult.IsSuccess, Is.True); - Assert.That(firstResult.Value, Is.EqualTo("ThisIsÄString")); + Assert.That(firstResult.Value, Is.EqualTo(InitialStringValue)); Assert.That(secondResult.IsSuccess, Is.True); - Assert.That(secondResult.Value, Is.EqualTo("ThisIsALongerStrîngWith文字化けチェック")); + Assert.That(secondResult.Value, Is.EqualTo(ExpectedFinalStringValue)); } /// @@ -917,7 +896,7 @@ public void ReadStringPointerOnKnownStringTest() [Test] public void ReadStringPointerWithZeroPointerTest() { - var result = TestProcessMemory!.ReadStringPointer(UIntPtr.Zero, DotNetStringSettings); + var result = TestProcessMemory!.ReadStringPointer(UIntPtr.Zero, GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnZeroPointer))); } @@ -929,10 +908,10 @@ public void ReadStringPointerWithZeroPointerTest() [Test] public void ReadStringPointerWithPointerToZeroPointerTest() { - // Arrange a space in memory that points to 0. + // Arrange a usable space in memory that contains only zeroes so that we can get a zero pointer. var allocatedSpace = TestProcessMemory!.Allocate(8, false).Value.ReserveRange(8).Value; - var result = TestProcessMemory!.ReadStringPointer(allocatedSpace.Address, DotNetStringSettings); + var result = TestProcessMemory!.ReadStringPointer(allocatedSpace.Address, GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnZeroPointer))); } @@ -944,7 +923,7 @@ public void ReadStringPointerWithPointerToZeroPointerTest() [Test] public void ReadStringPointerWithUnreadablePointerTest() { - var result = TestProcessMemory!.ReadStringPointer(UIntPtr.MaxValue, DotNetStringSettings); + var result = TestProcessMemory!.ReadStringPointer(GetMaxPointerValue(), GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnPointerReadFailure))); var failure = (StringReadFailureOnPointerReadFailure)result.Error; @@ -959,8 +938,7 @@ public void ReadStringPointerWithUnreadablePointerTest() [Test] public void ReadStringPointerWithPointerToUnreadableMemoryTest() { - var result = TestProcessMemory!.ReadStringPointer($"{OuterClassPointer:X}+10,10", - DotNetStringSettings); + var result = TestProcessMemory!.ReadStringPointer(GetPathToPointerToMaxAddress(), GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnStringBytesReadFailure))); var failure = (StringReadFailureOnStringBytesReadFailure)result.Error; @@ -977,7 +955,7 @@ public void ReadStringPointerWithInvalidSettingsTest() var settings = new StringSettings(Encoding.Unicode, isNullTerminated: false, lengthPrefix: null); - var result = TestProcessMemory!.ReadStringPointer(OuterClassPointer+8, settings); + var result = TestProcessMemory!.ReadStringPointer(GetAddressForValueAtIndex(IndexOfOutputString), settings); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnInvalidSettings))); } @@ -989,8 +967,7 @@ public void ReadStringPointerWithInvalidSettingsTest() [Test] public void ReadStringPointerWithBadPointerPathTest() { - var result = TestProcessMemory!.ReadStringPointer($"{OuterClassPointer:X}+10,10,0,0", - DotNetStringSettings); + var result = TestProcessMemory!.ReadStringPointer("bad pointer path", GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnPointerPathEvaluation))); var failure = (StringReadFailureOnPointerPathEvaluation)result.Error; @@ -1117,4 +1094,14 @@ public void ReadStringPointerWithTooLongStringWithoutPrefixTest() #endregion #endregion +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryReadTestX86 : ProcessMemoryReadTest +{ + /// Gets a boolean value defining which version of the target app is used. + protected override bool Is64Bit => false; } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs index 0827770..643b1d0 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs @@ -16,23 +16,22 @@ public class ProcessMemoryWriteTest : BaseProcessMemoryTest /// Index of the value to assert in the array representing the final output from the /// target app. /// Expected value of the final output from the target app. - [TestCase(true, IndexOfOutputOuterBool, "True")] - [TestCase((byte)94, IndexOfOutputOuterByte, "94")] - [TestCase((int)-447712345, IndexOfOutputOuterInt, "-447712345")] - [TestCase((uint)74753312, IndexOfOutputOuterUint, "74753312")] - [TestCase((long)-858884523, IndexOfOutputOuterLong, "-858884523")] - [TestCase((ulong)755443121891, IndexOfOutputOuterUlong, "755443121891")] + [TestCase(true, IndexOfOutputBool, "True")] + [TestCase((byte)94, IndexOfOutputByte, "94")] + [TestCase((int)-447712345, IndexOfOutputInt, "-447712345")] + [TestCase((uint)74753312, IndexOfOutputUInt, "74753312")] + [TestCase((long)-858884523, IndexOfOutputLong, "-858884523")] + [TestCase((ulong)755443121891, IndexOfOutputULong, "755443121891")] [TestCase((long)51356, IndexOfOutputInnerLong, "51356")] - [TestCase((short)-2421, IndexOfOutputOuterShort, "-2421")] - [TestCase((ushort)2594, IndexOfOutputOuterUshort, "2594")] - [TestCase((float)4474.783, IndexOfOutputOuterFloat, "4474.783")] - [TestCase((double)54234423.3147, IndexOfOutputOuterDouble, "54234423.3147")] - [TestCase(new byte[] { 0x8, 0x6, 0x4, 0xA }, IndexOfOutputOuterByteArray, "8,6,4,10")] + [TestCase((short)-2421, IndexOfOutputShort, "-2421")] + [TestCase((ushort)2594, IndexOfOutputUShort, "2594")] + [TestCase((float)4474.783, IndexOfOutputFloat, "4474.783")] + [TestCase((double)54234423.3147, IndexOfOutputDouble, "54234423.3147")] + [TestCase(new byte[] { 0x8, 0x6, 0x4, 0xA }, IndexOfOutputByteArray, "8,6,4,10")] public void WriteValueTest(object value, int finalResultsIndex, string expectedValue) { - var result = TestProcessMemory.Read(OuterClassPointer + (UIntPtr)0x10).Value; var pointerPath = GetPointerPathForValueAtIndex(finalResultsIndex); - ProceedToNextStep(); + ProceedToNextStep(); // Let the app overwrite the values once before writing TestProcessMemory!.Write(pointerPath, value); ProceedToNextStep(); AssertFinalResults(finalResultsIndex, expectedValue); @@ -69,8 +68,8 @@ public void WriteStructTest() [Test] public void WriteStringPointerTest() { - var pointerAddress = GetPointerPathForValueAtIndex(IndexOfOutputOuterString); - var stringSettings = TestProcessMemory!.FindStringSettings(pointerAddress, "ThisIsÄString").Value; + var pointerAddress = GetPointerPathForValueAtIndex(IndexOfOutputString); + var stringSettings = TestProcessMemory!.FindStringSettings(pointerAddress, InitialStringValue).Value; ProceedToNextStep(); // Make sure we make the test program change the string pointer before we overwrite it var newString = "This String Is Completely New And Also Longer Than The Original One"; @@ -80,7 +79,7 @@ public void WriteStringPointerTest() ProceedToNextStep(); // Makes the test program output the final results // Test that the program actually used (wrote to the console) the string that we hacked in - AssertFinalResults(IndexOfOutputOuterString, newString); + AssertFinalResults(IndexOfOutputString, newString); } } From 3e04ff12655d47f8752ec63fab80afe7f07814af Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sun, 23 Jun 2024 16:25:05 +0200 Subject: [PATCH 30/66] x86 tests for Attach & Evaluate features --- .../BaseProcessMemoryTest.cs | 23 +++++++++- .../ProcessMemoryAttachTest.cs | 10 +++++ .../ProcessMemoryEvaluateTest.cs | 44 +++++++++++++------ 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs b/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs index a201a26..b9aabd6 100644 --- a/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs @@ -289,11 +289,32 @@ protected PointerPath GetPathToRawStringBytes() /// Gets a pointer path that evaluates to an address pointing to a 0xFFFFFFFFFFFFFFFF (x64) or 0xFFFFFFFF (x86) /// value. Warning: the path itself does not evaluate to the max address, but to a pointer to it. To clarify with /// an example, this path will evaluate to say 0x4A64F850, and if you read a ulong at that address, you will get - /// 0xFFFFFFFFFFFFFFFF. + /// 0xFFFFFFFFFFFFFFFF. See for the former. /// protected PointerPath GetPathToPointerToMaxAddress() => OuterClassPointer.ToString("X") + (Is64Bit ? "+10,10" : "+20,10"); + /// + /// Gets a pointer path that evaluates to 0xFFFFFFFFFFFFFFFF (x64) or 0xFFFFFFFF (x86). + /// + protected PointerPath GetPathToMaxAddress() + => OuterClassPointer.ToString("X") + (Is64Bit ? "+10,10,0" : "+20,10,0"); + + /// + /// Gets a pointer path that evaluates to an address located one byte after 0xFFFFFFFFFFFFFFFF (x64) or 0xFFFFFFFF + /// (x86), which is out of bounds. This path should cause a failure on evaluation. + /// + protected PointerPath GetPathToPointerToMaxAddressPlusOne() + => OuterClassPointer.ToString("X") + (Is64Bit ? "+10,10,1" : "+20,10,1"); + + /// + /// Gets a pointer path that should not evaluate properly because it goes through the maximum possible address + /// (0xFFFFFFFFFFFFFFFF in x64 bits or 0xFFFFFFFF in x86) but does not stop there, which should cause a read failure + /// on evaluation. + /// + protected PointerPath GetPathToPointerThroughMaxAddress() + => OuterClassPointer.ToString("X") + (Is64Bit ? "+10,10,0,0" : "+20,10,0,0"); + /// /// Gets the address of the pointer of the output string of the target app, regardless of its bitness. /// diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs index 16e0501..8ceae48 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs @@ -50,4 +50,14 @@ public void ProcessDetachedOnExitTest() Assert.That(TestProcessMemory.IsAttached, Is.False); }); } +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryAttachTestX86 : ProcessMemoryAttachTest +{ + /// Gets a boolean value defining which version of the target app is used. + protected override bool Is64Bit => false; } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs index 514c18e..ba45d8e 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs @@ -17,46 +17,52 @@ public class ProcessMemoryEvaluateTest : BaseProcessMemoryTest [Test] public void EvaluateOnKnownPointerTest() { - // This path is known to point to 0xFFFFFFFFFFFFFFFF (i.e. the max 8-byte value). - var result = TestProcessMemory!.EvaluateMemoryAddress($"{OuterClassPointer:X}+10,10,0"); + var result = TestProcessMemory!.EvaluateMemoryAddress(GetPathToMaxAddress()); Assert.That(result.IsSuccess, Is.True); - Assert.That(result.Value, Is.EqualTo(unchecked((UIntPtr)ulong.MaxValue))); + Assert.That(result.Value, Is.EqualTo(GetMaxPointerValue())); } /// /// Tests an error case where the pointer path points to a value located after the last possible byte in memory /// (the maximum value of a UIntPtr + 1). - /// The operation is expected to fail with a . + /// The operation is expected to fail with a on an x64 + /// process, or a on an x86 process. /// [Test] public void EvaluateOverMaxPointerValueTest() { - var result = TestProcessMemory!.EvaluateMemoryAddress($"{OuterClassPointer:X}+10,10,1"); + var result = TestProcessMemory!.EvaluateMemoryAddress(GetPathToPointerToMaxAddressPlusOne()); Assert.That(result.IsSuccess, Is.False); var error = result.Error; - Assert.That(error, Is.TypeOf()); - var pathError = (PathEvaluationFailureOnPointerOutOfRange)error; - Assert.That(pathError.Offset, Is.EqualTo(new PointerOffset(1, false))); - Assert.That(pathError.PreviousAddress, Is.EqualTo(UIntPtr.MaxValue)); + + if (Is64Bit) + { + Assert.That(error, Is.TypeOf()); + var pathError = (PathEvaluationFailureOnPointerOutOfRange)error; + Assert.That(pathError.Offset, Is.EqualTo(new PointerOffset(1, false))); + Assert.That(pathError.PreviousAddress, Is.EqualTo(UIntPtr.MaxValue)); + } + else + Assert.That(error, Is.TypeOf()); } /// - /// Tests an error case where the pointer path points to a value that is not readable. + /// Tests an error case where the pointer path points to an unreachable value because one of the pointers in the + /// path (but not the last one) points to an unreadable address. /// The operation is expected to fail with a . /// [Test] - public void EvaluateWithUnreadableAddressTest() + public void EvaluateWithUnreadableAddressHalfwayThroughTest() { - // This path will try to follow a pointer to 0xFFFFFFFFFFFFFFFF, which is not readable - var result = TestProcessMemory!.EvaluateMemoryAddress($"{OuterClassPointer:X}+10,10,0,0"); + var result = TestProcessMemory!.EvaluateMemoryAddress(GetPathToPointerThroughMaxAddress()); Assert.That(result.IsSuccess, Is.False); var error = result.Error; Assert.That(error, Is.TypeOf()); var pathError = (PathEvaluationFailureOnPointerReadFailure)error; - Assert.That(pathError.Address, Is.EqualTo(unchecked((UIntPtr)ulong.MaxValue))); + Assert.That(pathError.Address, Is.EqualTo(GetMaxPointerValue())); Assert.That(pathError.Details, Is.TypeOf()); var readFailure = (ReadFailureOnSystemRead)pathError.Details; Assert.That(readFailure.Details, Is.TypeOf()); @@ -96,4 +102,14 @@ public void EvaluateWithUnknownModuleTest() var error = (PathEvaluationFailureOnBaseModuleNotFound)result.Error; Assert.That(error.ModuleName, Is.EqualTo("ThisModuleDoesNotExist.dll")); } +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryEvaluateTestX86 : ProcessMemoryEvaluateTest +{ + /// Gets a boolean value defining which version of the target app is used. + protected override bool Is64Bit => false; } \ No newline at end of file From 2cda0ccd25cdb1d5293c46a0174b7dd564f6da2a Mon Sep 17 00:00:00 2001 From: Doublevil Date: Fri, 28 Jun 2024 18:06:14 +0200 Subject: [PATCH 31/66] Injection rework for x86 & threading methods --- src/MindControl/Modules/PeParser.cs | 116 +++++++++++ src/MindControl/Modules/RemoteModule.cs | 51 +++++ .../Native/IOperatingSystemService.cs | 8 +- .../Native/Win32Service.Imports.cs | 9 + src/MindControl/Native/Win32Service.cs | 28 +-- .../ProcessMemory/ProcessMemory.Addressing.cs | 26 +-- .../ProcessMemory.CodeInjection.cs | 92 --------- .../ProcessMemory/ProcessMemory.Injection.cs | 68 +++++++ .../ProcessMemory/ProcessMemory.Thread.cs | 186 ++++++++++++++++++ src/MindControl/Results/InjectionFailure.cs | 68 +++---- src/MindControl/Results/ThreadFailure.cs | 127 ++++++++++++ .../Threading/ParameterizedRemoteThread.cs | 36 ++++ src/MindControl/Threading/RemoteThread.cs | 62 ++++++ .../BaseProcessMemoryTest.cs | 5 + .../ProcessMemoryFindBytesTest.cs | 24 +-- .../ProcessMemoryInjectionTest.cs | 15 +- .../ProcessMemoryThreadingTest.cs | 135 +++++++++++++ .../ProcessMemoryTests/RemoteModuleTest.cs | 59 ++++++ 18 files changed, 936 insertions(+), 179 deletions(-) create mode 100644 src/MindControl/Modules/PeParser.cs create mode 100644 src/MindControl/Modules/RemoteModule.cs delete mode 100644 src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs create mode 100644 src/MindControl/ProcessMemory/ProcessMemory.Injection.cs create mode 100644 src/MindControl/ProcessMemory/ProcessMemory.Thread.cs create mode 100644 src/MindControl/Results/ThreadFailure.cs create mode 100644 src/MindControl/Threading/ParameterizedRemoteThread.cs create mode 100644 src/MindControl/Threading/RemoteThread.cs create mode 100644 test/MindControl.Test/ProcessMemoryTests/ProcessMemoryThreadingTest.cs create mode 100644 test/MindControl.Test/ProcessMemoryTests/RemoteModuleTest.cs diff --git a/src/MindControl/Modules/PeParser.cs b/src/MindControl/Modules/PeParser.cs new file mode 100644 index 0000000..c086309 --- /dev/null +++ b/src/MindControl/Modules/PeParser.cs @@ -0,0 +1,116 @@ +using System.Text; +using MindControl.Results; + +namespace MindControl.Modules; + +/// +/// Parser for PE headers. +/// +/// Process memory instance to use to read memory. +/// Base address of the module to parse. +internal class PeParser(ProcessMemory processMemory, UIntPtr imageBaseAddress) +{ + /// Offset of the PE Header start address, relative to the start of the module. + private const int PeHeaderAddressOffset = 0x3C; + + /// + /// Reads and parses the export table of the module, associating the names of the exported functions with their + /// absolute addresses in the process memory. + /// + public Result, string> ReadExportTable() + { + var exportTable = new Dictionary(); + + // Read the PE header address + var peHeaderRva = processMemory.Read(imageBaseAddress + PeHeaderAddressOffset); + if (peHeaderRva.IsFailure) + return "Could not read the PE header address from the DOS header."; + var peHeaderAddress = imageBaseAddress + peHeaderRva.Value; + + // Read the magic number from the Optional Header + var optionalHeaderAddress = peHeaderAddress + 24; // Skip over the 20-byte File Header and 4-byte PE signature + var magicNumber = processMemory.Read(optionalHeaderAddress); + if (magicNumber.IsFailure) + return "Could not read the magic number."; + + bool? is64Bit = magicNumber.Value switch + { + 0x10B => false, // 32-bit + 0x20B => true, // 64-bit + _ => null + }; + + if (is64Bit == null) + return $"Invalid magic number value: 0x{magicNumber.Value:X}."; + + // Read the export table address + UIntPtr exportTableAddressPointer = peHeaderAddress + (UIntPtr)(is64Bit.Value ? 0x88 : 0x78); + var exportTableAddressRva = processMemory.Read(exportTableAddressPointer); + if (exportTableAddressRva.IsFailure) + return "Could not read the export table address."; + + // Read the export table size + var exportTableSize = processMemory.Read(exportTableAddressPointer + 4); + if (exportTableSize.IsFailure) + return "Could not read the export table size."; + + // Read the number of exported functions + var exportTableAddress = imageBaseAddress + exportTableAddressRva.Value; + var numberOfFunctions = processMemory.Read(exportTableAddress + 24); + if (numberOfFunctions.IsFailure) + return "Could not read the number of exported functions."; + + // Read the export name pointers table (ENPT) + var enptBytes = ReadExportTableBytes(exportTableAddress + 32, numberOfFunctions.Value * 4); + if (enptBytes == null) + return "Could not read the export name pointers table."; + + // Read the export ordinal table (EOT) + var eotBytes = ReadExportTableBytes(exportTableAddress + 36, numberOfFunctions.Value * 2); + if (eotBytes == null) + return "Could not read the export ordinal table."; + + // Read the export address table (EAT) + var eatBytes = ReadExportTableBytes(exportTableAddress + 28, numberOfFunctions.Value * 4); + if (eatBytes == null) + return "Could not read the export address table."; + + for (int i = 0; i < numberOfFunctions.Value; i++) + { + // Read the function name using the ENPT + var functionNameRva = BitConverter.ToUInt32(enptBytes, i * 4); + var functionNameResult = processMemory.ReadRawString(imageBaseAddress + functionNameRva, + Encoding.ASCII, 256); + if (functionNameResult.IsFailure) + continue; + var functionName = functionNameResult.Value; + + // Read the ordinal of the function from the EOT + var ordinal = BitConverter.ToUInt16(eotBytes, i * 2); + + // Read the address of the function from the EAT using the ordinal + var functionRva = BitConverter.ToUInt32(eatBytes, ordinal * 4); + var functionAddress = imageBaseAddress + functionRva; + + exportTable.TryAdd(functionName, functionAddress); + } + + return exportTable; + } + + /// + /// Reads the bytes of a table in the export table (ENPT, EOT or EAT). + /// + /// Address of the field holding the RVA to the table to read. + /// Size of the table to read. + /// The bytes of the table, or null if the table could not be read. + private byte[]? ReadExportTableBytes(UIntPtr exportTableRvaPointer, uint exportTableSize) + { + var exportTableRva = processMemory.Read(exportTableRvaPointer); + if (exportTableRva.IsFailure) + return null; + UIntPtr etAddress = imageBaseAddress + exportTableRva.Value; + var etBytesResult = processMemory.ReadBytes(etAddress, exportTableSize); + return etBytesResult.IsFailure ? null : etBytesResult.Value; + } +} \ No newline at end of file diff --git a/src/MindControl/Modules/RemoteModule.cs b/src/MindControl/Modules/RemoteModule.cs new file mode 100644 index 0000000..bb75c29 --- /dev/null +++ b/src/MindControl/Modules/RemoteModule.cs @@ -0,0 +1,51 @@ +using System.Diagnostics; +using MindControl.Results; + +namespace MindControl.Modules; + +/// +/// Represents a module loaded into another process. +/// +public class RemoteModule +{ + private readonly ProcessMemory _processMemory; + private readonly ProcessModule _internalModule; + + /// + /// Initializes a new instance of the class. + /// + /// Process memory instance initializing this module. + /// Managed module instance to wrap. + internal RemoteModule(ProcessMemory processMemory, ProcessModule internalModule) + { + _processMemory = processMemory; + _internalModule = internalModule; + } + + /// + /// Gets the managed module instance. + /// + /// The managed module instance. + public ProcessModule GetManagedModule() + => _internalModule; + + /// + /// Gets the range of memory occupied by the module. + /// + /// The memory range of the module. + public MemoryRange GetRange() + => MemoryRange.FromStartAndSize((UIntPtr)_internalModule.BaseAddress, (ulong)_internalModule.ModuleMemorySize); + + /// + /// Attempts to read the export table of the module, associating the names of the exported functions with their + /// absolute addresses in the process memory. This is useful to locate specific functions in a DLL, like Windows API + /// functions from kernel32.dll or user32.dll, or your own functions in a DLL you have injected into the process. + /// + /// A result holding either a dictionary containing the names and addresses of the exported functions, or + /// an error message in case the export table could not be read. + public Result, string> ReadExportTable() + { + var peParser = new PeParser(_processMemory, (UIntPtr)_internalModule.BaseAddress); + return peParser.ReadExportTable(); + } +} diff --git a/src/MindControl/Native/IOperatingSystemService.cs b/src/MindControl/Native/IOperatingSystemService.cs index 42529f3..bfc4344 100644 --- a/src/MindControl/Native/IOperatingSystemService.cs +++ b/src/MindControl/Native/IOperatingSystemService.cs @@ -113,13 +113,13 @@ Result CreateRemoteThread(IntPtr processHandle, UIntPtr s UIntPtr parameterAddress); /// - /// Waits for the specified thread to finish execution. + /// Waits for the specified thread to finish execution and returns its exit code. /// /// Handle of the target thread. /// Maximum time to wait for the thread to finish. - /// A result holding either a boolean indicating if the thread returned (True) or timed out (False), or a - /// system failure for other error cases. - Result WaitThread(IntPtr threadHandle, TimeSpan timeout); + /// A result holding either the exit code of the thread, or a thread failure when the operation failed. + /// + Result WaitThread(IntPtr threadHandle, TimeSpan timeout); /// /// Frees the memory allocated in the specified process for a region or a placeholder. diff --git a/src/MindControl/Native/Win32Service.Imports.cs b/src/MindControl/Native/Win32Service.Imports.cs index 8df110a..a05ca9c 100644 --- a/src/MindControl/Native/Win32Service.Imports.cs +++ b/src/MindControl/Native/Win32Service.Imports.cs @@ -366,6 +366,15 @@ private static class WaitForSingleObjectResult public static bool IsSuccessful(uint result) => result == Signaled; } + /// + /// Gets the exit code of the process with the given handle. + /// + /// A handle to the target thread. + /// A pointer to a variable to receive the exit code. + /// True if the function succeeds, or false otherwise. + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode); + /// /// Closes an open object handle. /// diff --git a/src/MindControl/Native/Win32Service.cs b/src/MindControl/Native/Win32Service.cs index 0654158..c954b76 100644 --- a/src/MindControl/Native/Win32Service.cs +++ b/src/MindControl/Native/Win32Service.cs @@ -358,25 +358,31 @@ public Result CreateRemoteThread(IntPtr processHandle, UI } /// - /// Waits for the specified thread to finish execution. + /// Waits for the specified thread to finish execution and returns its exit code. /// /// Handle of the target thread. /// Maximum time to wait for the thread to finish. - /// A result holding either a boolean indicating if the thread returned (True) or timed out (False), or a - /// system failure for other error cases. - public Result WaitThread(IntPtr threadHandle, TimeSpan timeout) + /// A result holding either the exit code of the thread, or a thread failure when the operation failed. + /// + public Result WaitThread(IntPtr threadHandle, TimeSpan timeout) { if (threadHandle == IntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(threadHandle), - "The thread handle is invalid (zero pointer)."); + return new ThreadFailureOnInvalidArguments("The thread handle is invalid (zero pointer)."); uint result = WaitForSingleObject(threadHandle, (uint)timeout.TotalMilliseconds); - if (WaitForSingleObjectResult.IsSuccessful(result)) - return true; + if (result == WaitForSingleObjectResult.Failed) + return new ThreadFailureOnSystemFailure("Failed to wait for the thread to finish execution.", + GetLastSystemError()); if (result == WaitForSingleObjectResult.Timeout) - return false; - - return GetLastSystemError(); + return new ThreadFailureOnWaitTimeout(); + if (!WaitForSingleObjectResult.IsSuccessful(result)) + return new ThreadFailureOnWaitAbandoned(); + + var exitCodeResult = GetExitCodeThread(threadHandle, out uint exitCode); + if (!exitCodeResult) + return new ThreadFailureOnSystemFailure("Failed to get the exit code of the thread.", + GetLastSystemError()); + return exitCode; } /// diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs index 5eff37b..14873bf 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using MindControl.Internal; +using MindControl.Modules; using MindControl.Results; namespace MindControl; @@ -114,17 +115,6 @@ public ProcessMemoryStream GetMemoryStream(UIntPtr startAddress) .Cast() .FirstOrDefault(m => string.Equals(m.ModuleName, moduleName, StringComparison.OrdinalIgnoreCase)); } - - /// - /// Gets the process module loaded in the attached process by its name. - /// - /// Name of the target module. - /// The module if found, null otherwise. - public ProcessModule? GetModule(string moduleName) - { - using var process = GetAttachedProcessInstance(); - return GetModule(moduleName, process); - } /// /// Gets the base address of the process module with the given name. @@ -134,7 +124,6 @@ public ProcessMemoryStream GetMemoryStream(UIntPtr startAddress) public UIntPtr? GetModuleAddress(string moduleName) { using var process = GetAttachedProcessInstance(); - var module = GetModule(moduleName, process); IntPtr? baseAddress = module?.BaseAddress; @@ -142,20 +131,15 @@ public ProcessMemoryStream GetMemoryStream(UIntPtr startAddress) } /// - /// Gets the memory range of the process module with the given name. + /// Gets the module with the given name, if it exists. /// /// Name of the target module. - /// The memory range of the module if found, null otherwise. - public MemoryRange? GetModuleRange(string moduleName) + /// The module if found, null otherwise. + public RemoteModule? GetModule(string moduleName) { using var process = GetAttachedProcessInstance(); - var module = GetModule(moduleName, process); - - if (module == null) - return null; - - return MemoryRange.FromStartAndSize((UIntPtr)(long)module.BaseAddress, (ulong)module.ModuleMemorySize - 1); + return module == null ? null : new RemoteModule(this, module); } /// diff --git a/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs b/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs deleted file mode 100644 index 74dae3d..0000000 --- a/src/MindControl/ProcessMemory/ProcessMemory.CodeInjection.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Text; -using MindControl.Native; -using MindControl.Results; - -namespace MindControl; - -// This partial class implements methods related to code injection. -public partial class ProcessMemory -{ - /// - /// Gets or sets the time to wait for the spawned thread to return when injecting a library using the - /// method. - /// The default value is 10 seconds. - /// - public TimeSpan LibraryInjectionThreadTimeout { get; set; } = TimeSpan.FromSeconds(10); - - /// - /// Injects a library into the attached process. - /// - /// Path to the library file to inject into the process. - /// A successful result, or an injection failure detailing how the operation failed. - public Result InjectLibrary(string libraryPath) - { - if (!IsAttached) - throw new InvalidOperationException(DetachedErrorMessage); - - // Check if the library file exists - string absoluteLibraryPath = Path.GetFullPath(libraryPath); - if (!File.Exists(absoluteLibraryPath)) - return new InjectionFailureOnLibraryFileNotFound(absoluteLibraryPath); - - // The goal here is to call the LoadLibrary function from inside the target process. - // We need to pass the address of the library path string as a parameter to the function. - // To do this, we first need to write the path of the library to load into the target process memory. - - // Write the library path string into the process memory - var libraryPathBytes = Encoding.Unicode.GetBytes(absoluteLibraryPath); - var allocateStringResult = _osService.AllocateMemory(ProcessHandle, libraryPathBytes.Length + 1, - MemoryAllocationType.Commit | MemoryAllocationType.Reserve, MemoryProtection.ReadWrite); - if (allocateStringResult.IsFailure) - return new InjectionFailureOnSystemFailure("Could not allocate memory to store the library file path.", - allocateStringResult.Error); - var allocatedLibPathAddress = allocateStringResult.Value; - - var writeStringResult = _osService.WriteProcessMemory(ProcessHandle, allocatedLibPathAddress, - libraryPathBytes); - if (writeStringResult.IsFailure) - return new InjectionFailureOnSystemFailure( - "Could not write the library file path to the target process memory.", - writeStringResult.Error); - - // Create a thread that runs in the target process to run the LoadLibrary function, using the address of - // the library path string as a parameter, so that it knows to load that library. - var loadLibraryAddressResult = _osService.GetLoadLibraryFunctionAddress(); - if (loadLibraryAddressResult.IsFailure) - return new InjectionFailureOnSystemFailure( - "Could not get the address of the LoadLibrary system API function from the current process.", - loadLibraryAddressResult.Error); - - var loadLibraryFunctionAddress = loadLibraryAddressResult.Value; - var threadHandleResult = _osService.CreateRemoteThread(ProcessHandle, loadLibraryFunctionAddress, - allocatedLibPathAddress); - if (threadHandleResult.IsFailure) - return new InjectionFailureOnSystemFailure( - "Could not create a remote thread in the target process to load the library.", - threadHandleResult.Error); - - var threadHandle = threadHandleResult.Value; - - // Wait for the thread to finish - var waitResult = _osService.WaitThread(threadHandle, LibraryInjectionThreadTimeout); - if (waitResult.IsFailure) - return new InjectionFailureOnSystemFailure("Could not wait for the thread to finish execution.", - waitResult.Error); - if (waitResult.Value == false) - return new InjectionFailureOnTimeout(); - - // Free the memory used for the library path string - _osService.ReleaseMemory(ProcessHandle, allocatedLibPathAddress); - - // Close the thread handle - _osService.CloseHandle(threadHandle); - - // Check that the module is correctly loaded. - // We do this because we don't know if the LoadLibrary function succeeded or not. - string expectedModuleName = Path.GetFileName(libraryPath); - if (GetModuleAddress(expectedModuleName) == null) - return new InjectionFailureOnModuleNotFound(); - - return Result.Success; - } -} \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs b/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs new file mode 100644 index 0000000..e59182d --- /dev/null +++ b/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs @@ -0,0 +1,68 @@ +using System.Text; +using MindControl.Native; +using MindControl.Results; + +namespace MindControl; + +// This partial class implements methods related to library injection. +public partial class ProcessMemory +{ + /// Default time to wait for the spawned thread to return when injecting a library. + private static readonly TimeSpan DefaultLibraryInjectionThreadTimeout = TimeSpan.FromSeconds(10); + + /// + /// Injects a library into the attached process. + /// + /// Path to the library file to inject into the process. + /// A successful result, or an injection failure detailing how the operation failed. + public Result InjectLibrary(string libraryPath) + => InjectLibrary(libraryPath, DefaultLibraryInjectionThreadTimeout); + + /// + /// Injects a library into the attached process. + /// + /// Path to the library file to inject into the process. + /// Time to wait for the injection thread to return. + /// A successful result, or an injection failure detailing how the operation failed. + public Result InjectLibrary(string libraryPath, TimeSpan waitTimeout) + { + if (!IsAttached) + throw new InvalidOperationException(DetachedErrorMessage); + + // Check if the library file exists + string absoluteLibraryPath = Path.GetFullPath(libraryPath); + if (!File.Exists(absoluteLibraryPath)) + return new InjectionFailureOnLibraryFileNotFound(absoluteLibraryPath); + + // Check if the module is already loaded + string expectedModuleName = Path.GetFileName(libraryPath); + if (GetModuleAddress(expectedModuleName) != null) + return new InjectionFailureOnModuleAlreadyLoaded(); + + // Store the library path string in the target process memory + var reservationResult = StoreString(absoluteLibraryPath, new StringSettings(Encoding.Unicode)); + if (reservationResult.IsFailure) + return new InjectionFailureOnParameterAllocation(reservationResult.Error); + var reservation = reservationResult.Value; + + // Run LoadLibraryW from inside the target process to have it load the library itself, which is usually safer + var threadResult = RunThread("kernel32.dll", "LoadLibraryW", reservation.Address); + if (threadResult.IsFailure) + return new InjectionFailureOnThreadFailure(threadResult.Error); + + // Wait for the thread to return + var waitResult = threadResult.Value.WaitForCompletion(waitTimeout); + if (waitResult.IsFailure) + return new InjectionFailureOnThreadFailure(waitResult.Error); + + // The exit code of the thread should be a handle to the loaded module. + // We don't need the handle, but we can use it to verify that the library was loaded successfully. + var moduleHandle = waitResult.Value; + if (moduleHandle == 0) + return new InjectionFailureOnLoadLibraryFailure(); + + reservation.Dispose(); // Free the parameter reservation as we don't need it anymore + + return Result.Success; + } +} \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Thread.cs b/src/MindControl/ProcessMemory/ProcessMemory.Thread.cs new file mode 100644 index 0000000..2323b2e --- /dev/null +++ b/src/MindControl/ProcessMemory/ProcessMemory.Thread.cs @@ -0,0 +1,186 @@ +using MindControl.Results; +using MindControl.Threading; + +namespace MindControl; + +// This partial class implements the thread-related features of ProcessMemory. +public partial class ProcessMemory +{ + #region Public methods + + /// + /// Starts a new thread in the target process, running the function at the specified address. This method will not + /// wait for the thread to finish execution. Use the resulting instance to wait for the + /// thread to finish if you need to. + /// + /// Address of the function (or start instruction) to run in the target process. + /// + /// An optional parameter to pass to the function. It can be either an address or a value, + /// depending on the function parameters. If the function takes multiple or complex parameters, consider using + /// and/or using a structure holding each parameter + /// with a matching field order. + /// A result holding either the thread instance that you can use to wait for the thread to return, or a + /// error. + public Result RunThread(UIntPtr functionAddress, UIntPtr? parameter = null) + { + var startResult = StartThread(functionAddress, parameter ?? UIntPtr.Zero); + if (startResult.IsFailure) + return startResult.Error; + return new RemoteThread(_osService, startResult.Value); + } + + /// + /// Starts a new thread in the target process, running the function at the address pointed by the given pointer + /// path. This method will not wait for the thread to finish execution. Use the resulting + /// instance to wait for the thread to finish if you need to. + /// + /// Pointer path to the function (or start instruction) to run in the target + /// process. + /// An optional parameter to pass to the function. It can be either an address or a value, + /// depending on the function parameters. If the function takes multiple or complex parameters, consider using + /// and/or using a structure holding each parameter + /// with a matching field order. + /// A result holding either the thread instance that you can use to wait for the thread to return, or a + /// error. + public Result RunThread(PointerPath functionPointerPath, UIntPtr? parameter = null) + { + var evaluateResult = EvaluateMemoryAddress(functionPointerPath); + if (evaluateResult.IsFailure) + return new ThreadFailureOnPointerPathEvaluation(evaluateResult.Error); + return RunThread(evaluateResult.Value, parameter); + } + + /// + /// Starts a new thread in the target process, running the specified exported function from a module loaded into the + /// target process. This method will not wait for the thread to finish execution. Use the resulting + /// instance to wait for the thread to finish if you need to. + /// + /// Name of the module containing the function to run (e.g. "kernel32.dll"). + /// Name of the exported function to run from the specified module. + /// An optional parameter to pass to the function. It can be either an address or a value, + /// depending on the function parameters. If the function takes multiple or complex parameters, consider using + /// and/or using a structure holding each parameter + /// with a matching field order. + /// A result holding either the thread instance that you can use to wait for the thread to return, or a + /// error. + public Result RunThread(string moduleName, string functionName, + UIntPtr? parameter = null) + { + var functionAddressResult = FindFunctionAddress(moduleName, functionName); + if (functionAddressResult.IsFailure) + return functionAddressResult.Error; + + return RunThread(functionAddressResult.Value, parameter); + } + + /// + /// Stores the given parameter in the target process memory, and then starts a new thread in the target process, + /// running the function at the specified address. This method will not wait for the thread to finish execution. + /// Use the resulting instance to wait for the thread to finish if you need to. + /// + /// Address of the function (or start instruction) to run in the target process. + /// + /// Parameter to pass to the function. If the function takes multiple parameters, they can + /// be arranged in a structure holding each value in the right field order. + /// A result holding either the thread instance that you can use to wait for the thread to return, or a + /// error. + public Result RunThreadWithStoredParameter(UIntPtr functionAddress, + T parameter) + { + var storeResult = Store(parameter); + if (storeResult.IsFailure) + return new ThreadFailureOnParameterStorageFailure(storeResult.Error); + + var reservation = storeResult.Value; + var startResult = StartThread(functionAddress, reservation.Address); + if (startResult.IsFailure) + return startResult.Error; + return new ParameterizedRemoteThread(_osService, startResult.Value, reservation); + } + + /// + /// Stores the given parameter in the target process memory, and then starts a new thread in the target process, + /// running the function at the address pointed by the specified pointer path. This method will not wait for the + /// thread to finish execution. Use the resulting instance to wait for the thread to + /// finish if you need to. + /// + /// Pointer path to the function (or start instruction) to run in the target + /// process. + /// Parameter to pass to the function. If the function takes multiple parameters, they can + /// be arranged in a structure holding each value in the right field order. + /// A result holding either the thread instance that you can use to wait for the thread to return, or a + /// error. + public Result RunThreadWithStoredParameter( + PointerPath functionPointerPath, T parameter) + { + var evaluateResult = EvaluateMemoryAddress(functionPointerPath); + if (evaluateResult.IsFailure) + return new ThreadFailureOnPointerPathEvaluation(evaluateResult.Error); + return RunThreadWithStoredParameter(evaluateResult.Value, parameter); + } + + /// + /// Stores the given parameter in the target process memory, and then starts a new thread in the target process, + /// running the specified exported function from a module loaded into the target process. This method will not wait + /// for the thread to finish execution. Use the resulting instance to wait for the + /// thread to finish if you need to. + /// + /// Name of the module containing the function to run (e.g. "kernel32.dll"). + /// Name of the exported function to run from the specified module. + /// Parameter to pass to the function. If the function takes multiple parameters, they can + /// be arranged in a structure holding each value in the right field order. + /// A result holding either the thread instance that you can use to wait for the thread to return, or a + /// error. + public Result RunThreadWithStoredParameter(string moduleName, + string functionName, T parameter) + { + var functionAddressResult = FindFunctionAddress(moduleName, functionName); + if (functionAddressResult.IsFailure) + return functionAddressResult.Error; + + return RunThreadWithStoredParameter(functionAddressResult.Value, parameter); + } + + #endregion + + #region Internal methods + + /// + /// Starts a new thread in the target process, running the function at the specified address. + /// + /// Address of the function (or start instruction) to run in the target process. + /// + /// Parameter to pass to the function. Use if the function does + /// not take parameters. + /// A result holding either the handle to the thread, or a error. + private Result StartThread(UIntPtr functionAddress, UIntPtr parameter) + { + var remoteThreadResult = _osService.CreateRemoteThread(ProcessHandle, functionAddress, parameter); + if (remoteThreadResult.IsFailure) + return new ThreadFailureOnSystemFailure("Failed to create the thread.", remoteThreadResult.Error); + return remoteThreadResult.Value; + } + + /// + /// Finds the address of the specified function in the export table of the specified module. + /// + /// Name of the module containing the function to find. + /// Name of the function to find in the export table of the module. + /// A result holding either the address of the function, or a error. + private Result FindFunctionAddress(string moduleName, string functionName) + { + var module = GetModule(moduleName); + if (module == null) + return new ThreadFailureOnFunctionNotFound($"The module \"{moduleName}\" is not loaded in the process."); + var exportTable = module.ReadExportTable(); + if (exportTable.IsFailure) + return new ThreadFailureOnFunctionNotFound( + $"Failed to read the export table of the module \"{moduleName}\": {exportTable.Error}."); + if (!exportTable.Value.TryGetValue(functionName, out UIntPtr functionAddress)) + return new ThreadFailureOnFunctionNotFound( + $"The function \"{functionName}\" was not found in the export table of the module \"{moduleName}\"."); + return functionAddress; + } + + #endregion +} \ No newline at end of file diff --git a/src/MindControl/Results/InjectionFailure.cs b/src/MindControl/Results/InjectionFailure.cs index 0eb030a..5faada0 100644 --- a/src/MindControl/Results/InjectionFailure.cs +++ b/src/MindControl/Results/InjectionFailure.cs @@ -5,25 +5,16 @@ /// public enum InjectionFailureReason { - /// - /// The library file to inject was not found. - /// + /// The library file to inject was not found. LibraryFileNotFound, - - /// - /// Failure when calling a system API function. - /// - SystemFailure, - - /// - /// The injection timed out. - /// - Timeout, - - /// - /// The library injection completed but the module cannot be found in the target process. - /// - ModuleNotFound + /// The module to inject is already loaded in the target process. + ModuleAlreadyLoaded, + /// Failure when trying to reserve memory to store function parameters. + ParameterAllocationFailure, + /// Failure when running the library loading thread. + ThreadFailure, + /// The library failed to load. + LoadLibraryFailure } /// @@ -45,40 +36,51 @@ public override string ToString() } /// -/// Represents a failure in an injection operation when calling a system API function. +/// Represents a failure in an injection operation when the module to inject is already loaded in the target process. +/// +public record InjectionFailureOnModuleAlreadyLoaded() + : InjectionFailure(InjectionFailureReason.ModuleAlreadyLoaded) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => "The module to inject is already loaded in the target process."; +} + +/// +/// Represents a failure in an injection operation when trying to reserve memory to store function parameters. /// -/// Message that explains the reason for the failure. /// Details about the failure. -public record InjectionFailureOnSystemFailure(string Message, SystemFailure Details) - : InjectionFailure(InjectionFailureReason.SystemFailure) +public record InjectionFailureOnParameterAllocation(AllocationFailure Details) + : InjectionFailure(InjectionFailureReason.ParameterAllocationFailure) { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"Failed to inject the library due to a system API failure: {Details}"; + => $"Failed to allocate memory to store function parameters required to inject the library: {Details}"; } /// -/// Represents a failure in an injection operation when the injection timed out. +/// Represents a failure in an injection operation when running the library loading thread. /// -public record InjectionFailureOnTimeout() - : InjectionFailure(InjectionFailureReason.Timeout) +/// Details about the failure. +public record InjectionFailureOnThreadFailure(ThreadFailure Details) + : InjectionFailure(InjectionFailureReason.ThreadFailure) { /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() - => "The injection timed out. The thread did not return in time."; + public override string ToString() + => $"Failed to inject the library due to a remote thread failure: {Details}"; } /// -/// Represents a failure in an injection operation when the library injection completed but the module cannot be found -/// in the target process. +/// Represents a failure in an injection operation when the library function call fails. /// -public record InjectionFailureOnModuleNotFound() - : InjectionFailure(InjectionFailureReason.ModuleNotFound) +public record InjectionFailureOnLoadLibraryFailure() + : InjectionFailure(InjectionFailureReason.LoadLibraryFailure) { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => "The library injection completed, but the module cannot be found in the target process. The injection thread has probably failed. Check that the library is valid and compatible with the target process (32-bit vs 64-bit)."; + => "The LoadLibraryW function returned a status code of 0, indicating failure. Check that the library is valid and compatible with the target process (32-bit vs 64-bit)."; } diff --git a/src/MindControl/Results/ThreadFailure.cs b/src/MindControl/Results/ThreadFailure.cs new file mode 100644 index 0000000..4648bde --- /dev/null +++ b/src/MindControl/Results/ThreadFailure.cs @@ -0,0 +1,127 @@ +namespace MindControl.Results; + +/// +/// Represents a reason for a thread operation to fail. +/// +public enum ThreadFailureReason +{ + /// Invalid arguments were provided to the thread operation. + InvalidArguments, + /// The thread handle has already been disposed. + DisposedInstance, + /// Failure when storing a thread parameter in the target process memory. + ParameterStorageFailure, + /// Failure when evaluating the pointer path to the target address. + PointerPathEvaluationFailure, + /// The target function cannot be found. + FunctionNotFound, + /// Failure when waiting for a thread to finish execution for too long. + ThreadWaitTimeout, + /// Failure when a waiting operation was abandoned. + WaitAbandoned, + /// Failure when calling a system API function. + SystemFailure +} + +/// +/// Represents a failure in a thread operation. +/// +public record ThreadFailure(ThreadFailureReason Reason); + +/// +/// Represents a failure in a thread operation when the arguments provided are invalid. +/// +/// Message that describes how the arguments fail to meet expectations. +public record ThreadFailureOnInvalidArguments(string Message) + : ThreadFailure(ThreadFailureReason.InvalidArguments) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The arguments provided are invalid: {Message}"; +} + +/// +/// Represents a failure in a thread operation when the thread handle has already been disposed. +/// +public record ThreadFailureOnDisposedInstance() + : ThreadFailure(ThreadFailureReason.DisposedInstance) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => "The thread handle has already been disposed."; +} + +/// +/// Represents a failure in a thread operation when storing a thread parameter in the target process memory. +/// +/// Details about the failure. +public record ThreadFailureOnParameterStorageFailure(AllocationFailure Details) + : ThreadFailure(ThreadFailureReason.ParameterStorageFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"Could not store the thread parameter in the target process memory: {Details}"; +} + +/// +/// Represents a failure in a thread operation when evaluating the pointer path to the target address. +/// +/// Details about the failure. +public record ThreadFailureOnPointerPathEvaluation(PathEvaluationFailure Details) + : ThreadFailure(ThreadFailureReason.PointerPathEvaluationFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failure when evaluating the pointer path to the target address: {Details}"; +} + +/// +/// Represents a failure in a thread operation when the target function cannot be found. +/// +/// Message including details about the failure. +public record ThreadFailureOnFunctionNotFound(string Message) + : ThreadFailure(ThreadFailureReason.FunctionNotFound) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Could not find the target function. {Message}"; +} + +/// +/// Represents a failure in a thread operation when the thread did not finish execution within the specified timeout. +/// +public record ThreadFailureOnWaitTimeout() + : ThreadFailure(ThreadFailureReason.ThreadWaitTimeout) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The thread did not finish execution within the specified timeout."; +} + +/// +/// Represents a failure in a thread operation when a waiting operation was abandoned. +/// +public record ThreadFailureOnWaitAbandoned() + : ThreadFailure(ThreadFailureReason.WaitAbandoned) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => "The waiting operation was abandoned."; +} + +/// +/// Represents a failure in a thread operation when invoking a system API function. +/// +/// Message that details what operation failed. +/// Details about the failure. +public record ThreadFailureOnSystemFailure(string Message, SystemFailure Details) + : ThreadFailure(ThreadFailureReason.SystemFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"A system API function call failed: {Message} / {Details}"; +} \ No newline at end of file diff --git a/src/MindControl/Threading/ParameterizedRemoteThread.cs b/src/MindControl/Threading/ParameterizedRemoteThread.cs new file mode 100644 index 0000000..fd2fa7b --- /dev/null +++ b/src/MindControl/Threading/ParameterizedRemoteThread.cs @@ -0,0 +1,36 @@ +using MindControl.Native; + +namespace MindControl.Threading; + +/// +/// Represents an awaitable thread running an operation in another process, with parameters temporarily stored in the +/// target process memory. +/// +public class ParameterizedRemoteThread : RemoteThread +{ + private readonly MemoryReservation _parameterReservation; + + /// Initializes a new instance of the class. + /// Operating system service used to interact with the thread. + /// Handle to the thread. + /// Memory reservation containing the parameters to pass to the thread. The + /// reservation will automatically be freed when the thread is awaited to completion or when this instance is + /// disposed. + internal ParameterizedRemoteThread(IOperatingSystemService osService, IntPtr threadHandle, + MemoryReservation parameterReservation) + : base(osService, threadHandle) + { + _parameterReservation = parameterReservation; + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged + /// resources. + public override void Dispose() + { + if (IsDisposed) + return; + + base.Dispose(); + _parameterReservation.Dispose(); + } +} \ No newline at end of file diff --git a/src/MindControl/Threading/RemoteThread.cs b/src/MindControl/Threading/RemoteThread.cs new file mode 100644 index 0000000..609db28 --- /dev/null +++ b/src/MindControl/Threading/RemoteThread.cs @@ -0,0 +1,62 @@ +using MindControl.Native; +using MindControl.Results; + +namespace MindControl.Threading; + +/// Represents an awaitable thread running an operation in another process. +public class RemoteThread : IDisposable +{ + private readonly IOperatingSystemService _osService; + private readonly IntPtr _threadHandle; + + /// Gets a value indicating whether the thread has been disposed. + protected bool IsDisposed { get; private set; } + + /// Initializes a new instance of the class. + /// Operating system service used to interact with the thread. + /// Handle to the thread. + internal RemoteThread(IOperatingSystemService osService, IntPtr threadHandle) + { + _osService = osService; + _threadHandle = threadHandle; + } + + /// + /// Synchronously waits for the thread to finish execution and returns its exit code. + /// + /// Maximum time to wait for the thread to finish execution. If the duration is exceeded, the + /// result will contain a error. + /// A result holding either the exit code of the thread, or a error. + public Result WaitForCompletion(TimeSpan timeout) + { + if (IsDisposed) + return new ThreadFailureOnDisposedInstance(); + + var waitResult = _osService.WaitThread(_threadHandle, timeout); + if (waitResult.IsFailure) + return waitResult.Error; + Dispose(); + return waitResult.Value; + } + + /// + /// Asynchronously waits for the thread to finish execution and returns its exit code. This method is just an + /// asynchronous wrapper around . + /// + /// Maximum time to wait for the thread to finish execution. If the duration is exceeded, the + /// result will contain a error. + /// A result holding either the exit code of the thread, or a error. + public Task> WaitForCompletionAsync(TimeSpan timeout) + => Task.Run(() => WaitForCompletion(timeout)); + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged + /// resources. + public virtual void Dispose() + { + if (IsDisposed) + return; + + _osService.CloseHandle(_threadHandle); + IsDisposed = true; + } +} \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs b/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs index b9aabd6..fe4cf58 100644 --- a/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs @@ -350,4 +350,9 @@ protected void AssertExpectedFinalResults() for (int i = 0; i < ExpectedFinalValues.Length; i++) Assert.That(FinalResults.ElementAtOrDefault(i), Is.EqualTo(ExpectedFinalValues[i])); } + + /// + /// Gets a boolean value indicating whether the target process has exited. + /// + protected bool HasProcessExited => _targetProcess?.HasExited ?? false; } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs index e951500..2876736 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs @@ -16,9 +16,7 @@ public class ProcessMemoryFindBytesTest : BaseProcessMemoryTest [Test] public void FindBytesWithKnownFixedBytesPatternTest() { - var range = TestProcessMemory!.GetModuleRange(MainModuleName); - Assert.That(range, Is.Not.Null); - + var range = TestProcessMemory!.GetModule(MainModuleName)!.GetRange(); var results = TestProcessMemory!.FindBytes("4D 79 53 74 72 69 6E 67 56 61 6C 75 65", range).ToArray(); // We won't verify the exact address, because it can change between runs and with modifications in the target @@ -28,7 +26,7 @@ public void FindBytesWithKnownFixedBytesPatternTest() Assert.That(results, Has.Length.EqualTo(1)); // Verify that the result is within the range of the main module - Assert.That(range!.Value.Contains(results.Single()), Is.True); + Assert.That(range.Contains(results.Single()), Is.True); } /// @@ -39,9 +37,7 @@ public void FindBytesWithKnownFixedBytesPatternTest() [Test] public void FindBytesWithKnownMaskedBytesPatternTest() { - var range = TestProcessMemory!.GetModuleRange(MainModuleName); - Assert.That(range, Is.Not.Null); - + var range = TestProcessMemory!.GetModule(MainModuleName)!.GetRange(); var results = TestProcessMemory!.FindBytes("4D 79 ?? ?? ?? ?? ?? ?? 56 61 6C 75 65", range).ToArray(); // We know there should be 3 occurrences of the pattern in the main module from observations with hacking tools @@ -55,7 +51,7 @@ public void FindBytesWithKnownMaskedBytesPatternTest() // Verify that the results are within the range of the main module foreach (var result in results) - Assert.That(range!.Value.Contains(result), Is.True); + Assert.That(range.Contains(result), Is.True); } /// @@ -66,9 +62,7 @@ public void FindBytesWithKnownMaskedBytesPatternTest() [Test] public void FindBytesWithKnownPartialMasksBytesPatternTest() { - var range = TestProcessMemory!.GetModuleRange(MainModuleName); - Assert.That(range, Is.Not.Null); - + var range = TestProcessMemory!.GetModule(MainModuleName)!.GetRange(); var results = TestProcessMemory!.FindBytes("4D 79 53 74 72 69 6E 6? ?6 61 6C 75 65", range).ToArray(); // We won't verify the exact address, because it can change between runs and with modifications in the target @@ -78,7 +72,7 @@ public void FindBytesWithKnownPartialMasksBytesPatternTest() Assert.That(results, Has.Length.EqualTo(1)); // Verify that the result is within the range of the main module - Assert.That(range!.Value.Contains(results.Single()), Is.True); + Assert.That(range.Contains(results.Single()), Is.True); } /// @@ -89,9 +83,7 @@ public void FindBytesWithKnownPartialMasksBytesPatternTest() [Test] public async Task FindBytesAsyncWithKnownFixedBytesPatternTest() { - var range = TestProcessMemory!.GetModuleRange(MainModuleName); - Assert.That(range, Is.Not.Null); - + var range = TestProcessMemory!.GetModule(MainModuleName)!.GetRange(); var results = await TestProcessMemory!.FindBytesAsync("4D 79 53 74 72 69 6E 67 56 61 6C 75 65", range) .ToArrayAsync(); @@ -102,6 +94,6 @@ public async Task FindBytesAsyncWithKnownFixedBytesPatternTest() Assert.That(results, Has.Length.EqualTo(1)); // Verify that the result is within the range of the main module - Assert.That(range!.Value.Contains(results.Single()), Is.True); + Assert.That(range.Contains(results.Single()), Is.True); } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs index f14896d..9a97de0 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs @@ -1,4 +1,5 @@ -using MindControl.Results; +using MindControl.Modules; +using MindControl.Results; using NUnit.Framework; namespace MindControl.Test.ProcessMemoryTests; @@ -42,7 +43,7 @@ public void SetUp() public void InjectLibraryTest() { var result = TestProcessMemory!.InjectLibrary(GetInjectedLibraryPath()); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, () => result.Error.ToString()); var output = ProceedToNextStep(); Assert.That(output, Is.EqualTo("Injected library attached")); } @@ -82,4 +83,14 @@ public void InjectLibraryWithLibraryFileNotFoundTest() Assert.That(error.LibraryPath, Has.Length.GreaterThan(path.Length)); // We expect a full path Assert.That(error.LibraryPath, Does.EndWith("NonExistentLibrary.dll")); } +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryInjectionTestX86 : ProcessMemoryInjectionTest +{ + /// Gets a boolean value defining which version of the target app is used. + protected override bool Is64Bit => false; } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryThreadingTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryThreadingTest.cs new file mode 100644 index 0000000..3c2fae7 --- /dev/null +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryThreadingTest.cs @@ -0,0 +1,135 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using Iced.Intel; +using MindControl.Results; +using MindControl.Threading; +using NUnit.Framework; + +namespace MindControl.Test.ProcessMemoryTests; + +/// +/// Tests the features of the class related to threads. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryThreadingTest : BaseProcessMemoryTest +{ + /// + /// Tests . + /// Runs the ExitProcess kernel32.dll function in a thread in the target process, and waits for the resulting + /// thread to end. + /// Check that the function has completed successfully and has triggered the process to exit. + /// + [Test] + public void RunThreadAndWaitTest() + { + var threadResult = TestProcessMemory!.RunThread("kernel32.dll", "ExitProcess"); + Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Error.ToString()); + var waitResult = threadResult.Value.WaitForCompletion(TimeSpan.FromSeconds(10)); + Assert.That(waitResult.IsSuccess, Is.True, () => waitResult.Error.ToString()); + Assert.That(waitResult.Value, Is.Zero); // The exit code should be 0 + Assert.That(HasProcessExited, Is.True); // The process should have exited as a result of the function call + } + + /// + /// Tests . + /// Writes a simple assembly code that calls the GetCurrentDirectoryW kernel32.dll function, runs that code in a + /// thread, and waits for the resulting thread to end. + /// We should be able to get the current directory of the target process. + /// + [Test] + public void RunThreadWithMultipleParametersTest() + { + var bufferReservation = TestProcessMemory!.Reserve(2048, false).Value; + var kernel32Module = TestProcessMemory.GetModule("kernel32.dll"); + var functionAddress = kernel32Module!.ReadExportTable().Value["GetCurrentDirectoryW"]; + + // We cannot call GetCurrentDirectoryW directly because of its parameters. We need to write a trampoline + // function that arranges the parameters in the right registers and then calls the target function. + var assembler = new Assembler(64); + var rcx = new AssemblerRegister64(Register.RCX); + var rdx = new AssemblerRegister64(Register.RDX); + // Use the fastcall calling convention, i.e. RCX and RDX are used for the first two arguments + assembler.mov(rcx, 2048); // Write the buffer size + assembler.mov(rdx, bufferReservation.Address.ToUInt64()); // Write the buffer address + assembler.call(functionAddress.ToUInt64()); // Call GetCurrentDirectoryW + assembler.ret(); + + // Write the code to the target process + var codeReservation = TestProcessMemory.Reserve(256, true, nearAddress: kernel32Module.GetRange().Start).Value; + var bytes = assembler.AssembleToBytes(codeReservation.Address).Value; + TestProcessMemory.Write(codeReservation.Address, bytes); + + // Run the thread + var threadResult = TestProcessMemory!.RunThread(codeReservation.Address); + Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Error.ToString()); + var waitResult = threadResult.Value.WaitForCompletion(TimeSpan.FromSeconds(10)); + Assert.That(waitResult.IsSuccess, Is.True, () => waitResult.Error.ToString()); + + // Read the resulting string from the allocated buffer + var resultingString = TestProcessMemory.ReadRawString(bufferReservation.Address, Encoding.Unicode, 512).Value; + Assert.That(resultingString, Is.EqualTo(Environment.CurrentDirectory)); + } + + /// + /// Tests . + /// Runs the Sleep kernel32.dll function in a thread in the target process. + /// Waits for the resulting thread, but with a timeout that does not leave enough time for the function to complete. + /// Check that the wait operation returns a error. + /// + [Test] + public void RunThreadSleepTimeoutTest() + { + // Start a Sleep thread that will run for 5 seconds, but wait only for 500ms + var threadResult = TestProcessMemory!.RunThread("kernel32.dll", "Sleep", 5000); + Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Error.ToString()); + var waitResult = threadResult.Value.WaitForCompletion(TimeSpan.FromMilliseconds(500)); + Assert.That(waitResult.IsSuccess, Is.False); + Assert.That(waitResult.Error, Is.TypeOf()); + } + + /// + /// Tests + /// and . + /// Runs the Sleep kernel32.dll function in a thread in the target process a bunch of times. + /// Waits for all resulting threads asynchronously. + /// Checks that all threads complete successfully in a timely manner. + /// + [Test] + public async Task RunThreadSleepWaitAsyncTest() + { + var tasks = new List>>(); + for (int i = 0; i < 10; i++) + { + // Each thread executes Sleep for 500ms + var threadResult = TestProcessMemory!.RunThread("kernel32.dll", "Sleep", 500); + Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Error.ToString()); + tasks.Add(threadResult.Value.WaitForCompletionAsync(TimeSpan.FromSeconds(10))); + } + + var stopwatch = Stopwatch.StartNew(); + await Task.WhenAll(tasks); + stopwatch.Stop(); + + foreach (var task in tasks) + { + Assert.That(task.Result.IsSuccess, Is.True, () => task.Result.Error.ToString()); + Assert.That(task.Result.Value, Is.Zero); + } + + // We check that threads run concurrently by checking that waiting for all of them takes less than n times the + // expected completion time of a single thread. + // Make sure to keep some leeway for the test to pass consistently, even in environments with scarce resources. + Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(2000)); + } +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryThreadingTestX86 : ProcessMemoryThreadingTest +{ + /// Gets a boolean value defining which version of the target app is used. + protected override bool Is64Bit => false; +} \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/RemoteModuleTest.cs b/test/MindControl.Test/ProcessMemoryTests/RemoteModuleTest.cs new file mode 100644 index 0000000..33e0005 --- /dev/null +++ b/test/MindControl.Test/ProcessMemoryTests/RemoteModuleTest.cs @@ -0,0 +1,59 @@ +using MindControl.Modules; +using NUnit.Framework; + +namespace MindControl.Test.ProcessMemoryTests; + +/// +/// Tests the features of the class. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class RemoteModuleTest : BaseProcessMemoryTest +{ + /// + /// Tests the method with an invalid module name. + /// + [Test] + public void GetModuleWithInvalidNameTest() + { + var module = TestProcessMemory!.GetModule("this module does not exist.dll"); + Assert.That(module, Is.Null); + } + + /// + /// Tests the method on an existing module. + /// + [Test] + public void GetRangeTest() + { + var module = TestProcessMemory!.GetModule("kernel32.dll") ?? throw new Exception("Module not found"); + var range = module.GetRange(); + Assert.That(range.Start, Is.EqualTo((UIntPtr)module.GetManagedModule().BaseAddress)); + Assert.That(range.GetSize(), Is.EqualTo((ulong)module.GetManagedModule().ModuleMemorySize)); + } + + /// + /// Tests the method on kernel32.dll. + /// Expect more than 1000 functions (kernel32.dll is packed up!) and that in particular LoadLibraryW is among them. + /// Also check that the address of functions are within the bounds of the module. + /// + [Test] + public void ReadExportTableWithKernel32Test() + { + var module = TestProcessMemory!.GetModule("kernel32.dll") ?? throw new Exception("Module not found"); + var exportTable = module.ReadExportTable(); + Assert.That(exportTable.IsSuccess, Is.True, () => exportTable.Error); + Assert.That(exportTable.Value, Has.Count.GreaterThan(1000)); + Assert.That(exportTable.Value.ContainsKey("LoadLibraryW")); + Assert.That(exportTable.Value.Values.Select(t => module.GetRange().Contains(t)), Is.All.True); + } +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class RemoteModuleTestX86 : RemoteModuleTest +{ + /// Gets a boolean value defining which version of the target app is used. + protected override bool Is64Bit => false; +} \ No newline at end of file From f802ac355397f1daebe7600b338c1728c8fa6461 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 29 Jun 2024 22:50:29 +0200 Subject: [PATCH 32/66] Various fixes, adjustments and tests --- .../Code/ProcessMemory.Code.cs | 59 ++++- .../Hooks/ProcessMemory.Hooks.cs | 43 +++- .../Results/CodeWritingFailure.cs | 42 ++-- src/MindControl.Code/Results/HookFailure.cs | 52 ++-- src/MindControl/Addressing/PointerOffset.cs | 6 +- .../Allocation/MemoryAllocation.cs | 21 +- .../Internal/ConversionExtensions.cs | 25 +- .../ProcessMemory/ProcessMemory.Addressing.cs | 3 + .../ProcessMemory/ProcessMemory.Allocation.cs | 72 ++++-- .../ProcessMemory/ProcessMemory.Injection.cs | 5 +- .../ProcessMemory/ProcessMemory.Read.cs | 58 ++++- ...y.Thread.cs => ProcessMemory.Threading.cs} | 113 +++------ .../ProcessMemory/ProcessMemory.Write.cs | 59 ++++- .../ProcessMemory/ProcessMemory.cs | 4 - src/MindControl/Results/AllocationFailure.cs | 32 +-- src/MindControl/Results/Failure.cs | 11 + .../Results/FindStringSettingsFailure.cs | 41 ++-- src/MindControl/Results/InjectionFailure.cs | 29 ++- .../Results/PathEvaluationFailure.cs | 36 +-- src/MindControl/Results/ReadFailure.cs | 50 ++-- src/MindControl/Results/ReservationFailure.cs | 26 +- src/MindControl/Results/StoreFailure.cs | 69 ++++++ src/MindControl/Results/StringReadFailure.cs | 55 ++--- src/MindControl/Results/ThreadFailure.cs | 34 ++- src/MindControl/Results/WriteFailure.cs | 90 +++++-- .../Threading/ParameterizedRemoteThread.cs | 36 --- .../AddressingTests/PointerPathTest.cs | 18 ++ .../AllocationTests/MemoryAllocationTest.cs | 15 +- .../ProcessMemoryCodeExtensionsTest.cs | 26 ++ .../ProcessMemoryHookExtensionsTest.cs | 78 +++++- .../ProcessMemoryAllocationTest.cs | 83 ++++++- .../ProcessMemoryEvaluateTest.cs | 13 + .../ProcessMemoryInjectionTest.cs | 18 +- .../ProcessMemoryReadTest.cs | 230 ++++++++++++++++++ .../ProcessMemoryThreadingTest.cs | 219 +++++++++++++++-- .../ProcessMemoryWriteTest.cs | 90 ++++++- .../SearchTests/ByteSearchPatternTest.cs | 20 +- 37 files changed, 1387 insertions(+), 494 deletions(-) rename src/MindControl/ProcessMemory/{ProcessMemory.Thread.cs => ProcessMemory.Threading.cs} (51%) create mode 100644 src/MindControl/Results/Failure.cs create mode 100644 src/MindControl/Results/StoreFailure.cs delete mode 100644 src/MindControl/Threading/ParameterizedRemoteThread.cs diff --git a/src/MindControl.Code/Code/ProcessMemory.Code.cs b/src/MindControl.Code/Code/ProcessMemory.Code.cs index 5731551..e46df9e 100644 --- a/src/MindControl.Code/Code/ProcessMemory.Code.cs +++ b/src/MindControl.Code/Code/ProcessMemory.Code.cs @@ -9,7 +9,60 @@ namespace MindControl.Code; public static class ProcessMemoryCodeExtensions { /// NOP instruction opcode. - public const byte NopByte = 0x90; + internal const byte NopByte = 0x90; + + /// Maximum byte count for a single instruction. + internal const int MaxInstructionLength = 15; + + /// + /// Assembles and stores the instructions registered in the given assembler as executable code in the process + /// memory. If needed, memory is allocated to store the data. Returns the reservation that holds the data. + /// + /// Process memory instance to use. + /// Assembler holding the instructions to store. + /// + /// A result holding either the reservation where the code has been written, or an allocation failure. + /// + public static Result StoreCode(this ProcessMemory processMemory, + Assembler assembler, UIntPtr? nearAddress = null) + { + if (!processMemory.IsAttached) + return new StoreFailureOnDetachedProcess(); + if (!assembler.Instructions.Any()) + return new StoreFailureOnInvalidArguments("The given assembler has no instructions to assemble."); + + // The length of the assembled code will vary depending on where we store it (because of relative operands). + // So the problem is, we need a memory reservation to assemble the code, but we need to assemble the code to + // know how much memory to reserve. + // Fortunately, we know that instructions are at most 15 bytes long, and we know the instruction count, so we + // can start by reserving the maximum possible length of the assembled code. + var codeMaxLength = assembler.Instructions.Count * MaxInstructionLength; + var reservationResult = processMemory.Reserve((ulong)codeMaxLength, true, nearAddress: nearAddress); + if (reservationResult.IsFailure) + return new StoreFailureOnAllocation(reservationResult.Error); + + var reservation = reservationResult.Value; + + // Once we have the reservation, we can assemble it, using its address as a base address. + var assemblyResult = assembler.AssembleToBytes(reservation.Address); + if (assemblyResult.IsFailure) + return new StoreFailureOnInvalidArguments( + $"The given assembler failed to assemble the code: {assemblyResult.Error}"); + + // Shrink the reservation to the actual size of the assembled code to avoid wasting memory as much as possible. + var assembledCode = assemblyResult.Value; + reservation.Shrink((ulong)(codeMaxLength - assembledCode.Length)); + + // Write the assembled code to the reservation. + var writeResult = processMemory.WriteBytes(reservation.Address, assembledCode, MemoryProtectionStrategy.Ignore); + if (writeResult.IsFailure) + { + reservation.Dispose(); + return new StoreFailureOnWrite(writeResult.Error); + } + + return reservation; + } /// /// Replaces the instruction (or multiple consecutive instructions) referenced by the given pointer path with NOP @@ -23,6 +76,8 @@ public static class ProcessMemoryCodeExtensions public static Result DisableCodeAt(this ProcessMemory processMemory, PointerPath pointerPath, int instructionCount = 1) { + if (!processMemory.IsAttached) + return new CodeWritingFailureOnDetachedProcess(); if (instructionCount < 1) return new CodeWritingFailureOnInvalidArguments( "The number of instructions to replace must be at least 1."); @@ -46,6 +101,8 @@ public static Result DisableCodeAt(this ProcessM public static Result DisableCodeAt(this ProcessMemory processMemory, UIntPtr address, int instructionCount = 1) { + if (!processMemory.IsAttached) + return new CodeWritingFailureOnDetachedProcess(); if (address == UIntPtr.Zero) return new CodeWritingFailureOnZeroPointer(); if (instructionCount < 1) diff --git a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs index 2fd0524..ad0b8b2 100644 --- a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs +++ b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs @@ -20,9 +20,6 @@ public static class ProcessMemoryHookExtensions /// code, and aligned properly. /// private const int FarJumpInstructionLength = 15; - - /// Maximum byte count for a single instruction. - private const int MaxInstructionLength = 15; #region Public hook methods @@ -49,6 +46,9 @@ public static class ProcessMemoryHookExtensions public static Result Hook(this ProcessMemory processMemory, PointerPath targetInstructionPointerPath, byte[] code, HookOptions options) { + if (!processMemory.IsAttached) + return new HookFailureOnDetachedProcess(); + var addressResult = processMemory.EvaluateMemoryAddress(targetInstructionPointerPath); if (addressResult.IsFailure) return new HookFailureOnPathEvaluation(addressResult.Error); @@ -59,8 +59,8 @@ public static Result Hook(this ProcessMemory processMemor /// /// Injects code into the target process to be executed when the instruction at the given executable address is /// reached. Depending on the options, the injected code may replace the original target instruction, or get - /// executed either before or after it. If specified, additional instructions that save and restore registers will be - /// added to the injected code. + /// executed either before or after it. If specified, additional instructions that save and restore registers will + /// be added to the injected code. /// Execution of the original code will then continue normally (unless the provided code is designed otherwise). /// This signature uses a byte array containing the code to inject. If your code contains instructions with relative /// operands (like jumps or calls), they may not point to the intended address. In these cases, prefer the @@ -78,6 +78,8 @@ public static Result Hook(this ProcessMemory processMemor public static Result Hook(this ProcessMemory processMemory, UIntPtr targetInstructionAddress, byte[] code, HookOptions options) { + if (!processMemory.IsAttached) + return new HookFailureOnDetachedProcess(); if (targetInstructionAddress == UIntPtr.Zero) return new HookFailureOnZeroPointer(); if (code.Length == 0) @@ -115,6 +117,9 @@ public static Result Hook(this ProcessMemory processMemor public static Result Hook(this ProcessMemory processMemory, PointerPath targetInstructionPointerPath, Assembler codeAssembler, HookOptions options) { + if (!processMemory.IsAttached) + return new HookFailureOnDetachedProcess(); + var addressResult = processMemory.EvaluateMemoryAddress(targetInstructionPointerPath); if (addressResult.IsFailure) return new HookFailureOnPathEvaluation(addressResult.Error); @@ -125,8 +130,8 @@ public static Result Hook(this ProcessMemory processMemor /// /// Injects code into the target process to be executed when the instruction at the given executable address is /// reached. Depending on the options, the injected code may replace the original target instruction, or get - /// executed either before or after it. If specified, additional instructions that save and restore registers will be - /// added to the injected code. + /// executed either before or after it. If specified, additional instructions that save and restore registers will + /// be added to the injected code. /// Execution of the original code will then continue normally (unless the provided code is designed otherwise). /// This signature uses an assembler, which is recommended, especially if your code contains instructions with /// relative operands (like jumps or calls), because the assembler will adjust addresses to guarantee they point to @@ -142,6 +147,8 @@ public static Result Hook(this ProcessMemory processMemor public static Result Hook(this ProcessMemory processMemory, UIntPtr targetInstructionAddress, Assembler codeAssembler, HookOptions options) { + if (!processMemory.IsAttached) + return new HookFailureOnDetachedProcess(); if (targetInstructionAddress == UIntPtr.Zero) return new HookFailureOnZeroPointer(); if (codeAssembler.Instructions.Count == 0) @@ -150,7 +157,7 @@ public static Result Hook(this ProcessMemory processMemor // The problem with using an assembler is that we don't know how many bytes the assembled code will take, so we // have to use the most conservative estimate possible, which is the maximum length of an instruction multiplied // by the number of instructions in the assembler. - int codeMaxLength = MaxInstructionLength * codeAssembler.Instructions.Count; + int codeMaxLength = ProcessMemoryCodeExtensions.MaxInstructionLength * codeAssembler.Instructions.Count; ulong sizeToReserve = GetSizeToReserveForCodeInjection(processMemory, codeMaxLength, options); // Reserve memory for the injected code, as close as possible to the target instruction @@ -314,6 +321,9 @@ public static Result ReplaceCodeAt(this ProcessMemory p PointerPath targetInstructionPointerPath, int instructionCount, byte[] code, params HookRegister[] registersToPreserve) { + if (!processMemory.IsAttached) + return new HookFailureOnDetachedProcess(); + var addressResult = processMemory.EvaluateMemoryAddress(targetInstructionPointerPath); if (addressResult.IsFailure) return new HookFailureOnPathEvaluation(addressResult.Error); @@ -342,6 +352,8 @@ public static Result ReplaceCodeAt(this ProcessMemory p public static Result ReplaceCodeAt(this ProcessMemory processMemory, UIntPtr targetInstructionAddress, int instructionCount, byte[] code, params HookRegister[] registersToPreserve) { + if (!processMemory.IsAttached) + return new HookFailureOnDetachedProcess(); if (targetInstructionAddress == UIntPtr.Zero) return new HookFailureOnZeroPointer(); if (instructionCount < 1) @@ -401,6 +413,9 @@ public static Result ReplaceCodeAt(this ProcessMemory p PointerPath targetInstructionPointerPath, int instructionCount, Assembler codeAssembler, params HookRegister[] registersToPreserve) { + if (!processMemory.IsAttached) + return new HookFailureOnDetachedProcess(); + var addressResult = processMemory.EvaluateMemoryAddress(targetInstructionPointerPath); if (addressResult.IsFailure) return new HookFailureOnPathEvaluation(addressResult.Error); @@ -430,6 +445,8 @@ public static Result ReplaceCodeAt(this ProcessMemory p UIntPtr targetInstructionAddress, int instructionCount, Assembler codeAssembler, params HookRegister[] registersToPreserve) { + if (!processMemory.IsAttached) + return new HookFailureOnDetachedProcess(); if (targetInstructionAddress == UIntPtr.Zero) return new HookFailureOnZeroPointer(); if (instructionCount < 1) @@ -446,7 +463,8 @@ public static Result ReplaceCodeAt(this ProcessMemory p var targetAddressAssemblyResult = codeAssembler.AssembleToBytes( targetInstructionAddress + (UIntPtr)preCodeSize, 128); if (targetAddressAssemblyResult.IsFailure) - return new HookFailureOnCodeAssembly(HookCodeAssemblySource.InjectedCode, targetAddressAssemblyResult.Error); + return new HookFailureOnCodeAssembly(HookCodeAssemblySource.InjectedCode, + targetAddressAssemblyResult.Error); var fullCodeAssemblyAtTargetAddressResult = AssembleFullCodeToInject(processMemory, targetAddressAssemblyResult.Value, targetInstructionAddress, hookOptions); if (fullCodeAssemblyAtTargetAddressResult.IsFailure) @@ -461,7 +479,7 @@ public static Result ReplaceCodeAt(this ProcessMemory p // The code to inject is larger than the original instructions, so we need to perform a hook // Reserve memory - int codeMaxLength = MaxInstructionLength * codeAssembler.Instructions.Count; + int codeMaxLength = ProcessMemoryCodeExtensions.MaxInstructionLength * codeAssembler.Instructions.Count; ulong sizeToReserve = GetSizeToReserveForCodeInjection(processMemory, codeMaxLength, hookOptions); var reservationResult = ReserveHookTarget(processMemory, sizeToReserve, targetInstructionAddress, hookOptions.JumpMode); @@ -805,11 +823,12 @@ private static Result BuildPostHookCode(ulong baseAddress, /// Maximum length in bytes of the code instructions to inject. /// Options defining how the hook behaves. /// The size to reserve in bytes. - private static ulong GetSizeToReserveForCodeInjection(ProcessMemory processMemory, int codeLength, HookOptions options) + private static ulong GetSizeToReserveForCodeInjection(ProcessMemory processMemory, int codeLength, + HookOptions options) { return (ulong)(options.GetExpectedGeneratedCodeSize(processMemory.Is64Bit) + codeLength - + MaxInstructionLength // Extra room for the original instructions + + ProcessMemoryCodeExtensions.MaxInstructionLength // Extra room for the original instruction(s) + FarJumpInstructionLength); // Extra room for the jump back to the original code } diff --git a/src/MindControl.Code/Results/CodeWritingFailure.cs b/src/MindControl.Code/Results/CodeWritingFailure.cs index e4da14d..7a331fa 100644 --- a/src/MindControl.Code/Results/CodeWritingFailure.cs +++ b/src/MindControl.Code/Results/CodeWritingFailure.cs @@ -7,34 +7,19 @@ namespace MindControl.Results; /// public enum CodeWritingFailureReason { - /// - /// The given pointer path could not be successfully evaluated. - /// + /// The target process is not attached. + DetachedProcess, + /// The given pointer path could not be successfully evaluated. PathEvaluationFailure, - - /// - /// The arguments provided to the code write operation are invalid. - /// + /// The arguments provided to the code write operation are invalid. InvalidArguments, - - /// - /// The target address is a zero pointer. - /// + /// The target address is a zero pointer. ZeroPointer, - - /// - /// A reading operation failed. - /// + /// A reading operation failed. ReadFailure, - - /// - /// A code disassembling operation failed. - /// + /// A code disassembling operation failed. DecodingFailure, - - /// - /// A write operation failed. - /// + /// A write operation failed. WriteFailure } @@ -44,6 +29,17 @@ public enum CodeWritingFailureReason /// Reason for the failure. public abstract record CodeWritingFailure(CodeWritingFailureReason Reason); +/// +/// Represents a failure that occurred while writing code to a target process when the target process is not attached. +/// +public record CodeWritingFailureOnDetachedProcess() + : CodeWritingFailure(CodeWritingFailureReason.DetachedProcess) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => Failure.DetachedErrorMessage; +} + /// /// Represents a failure that occurred while writing code to a target process when the pointer path failed to evaluate. /// diff --git a/src/MindControl.Code/Results/HookFailure.cs b/src/MindControl.Code/Results/HookFailure.cs index dd2da7e..ada3e24 100644 --- a/src/MindControl.Code/Results/HookFailure.cs +++ b/src/MindControl.Code/Results/HookFailure.cs @@ -7,44 +7,23 @@ namespace MindControl.Results; /// public enum HookFailureReason { - /// - /// The given pointer path could not be successfully evaluated. - /// + /// The target process is not attached. + DetachedProcess, + /// The given pointer path could not be successfully evaluated. PathEvaluationFailure, - - /// - /// The target address is a zero pointer. - /// + /// The target address is a zero pointer. ZeroPointer, - - /// - /// The arguments provided to the hook operation are invalid. - /// + /// The arguments provided to the hook operation are invalid. InvalidArguments, - - /// - /// The memory allocation operation failed. - /// + /// The memory allocation operation failed. AllocationFailure, - - /// - /// A reading operation failed. - /// + /// A reading operation failed. ReadFailure, - - /// - /// A code disassembling operation failed. - /// + /// A code disassembling operation failed. DecodingFailure, - - /// - /// Instructions could not be assembled into a code block. - /// + /// Instructions could not be assembled into a code block. CodeAssemblyFailure, - - /// - /// A write operation failed. - /// + /// A write operation failed. WriteFailure } @@ -54,6 +33,17 @@ public enum HookFailureReason /// Reason for the failure. public abstract record HookFailure(HookFailureReason Reason); +/// +/// Represents a failure that occurred in a hook operation when the target process is not attached. +/// +public record HookFailureOnDetachedProcess() + : HookFailure(HookFailureReason.DetachedProcess) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => Failure.DetachedErrorMessage; +} + /// /// Represents a failure that occurred in a hook operation when the pointer path failed to evaluate. /// diff --git a/src/MindControl/Addressing/PointerOffset.cs b/src/MindControl/Addressing/PointerOffset.cs index 654fbd6..96d7ede 100644 --- a/src/MindControl/Addressing/PointerOffset.cs +++ b/src/MindControl/Addressing/PointerOffset.cs @@ -60,8 +60,8 @@ public readonly record struct PointerOffset(ulong Offset, bool IsNegative) /// The offset address, or null if the result overflows or is negative. public UIntPtr? OffsetAddress(UIntPtr address) { - var sum = Plus((ulong)address, false); - if (sum == null || sum.Value.IsNegative || sum.Value.Offset > (ulong)UIntPtr.MaxValue) + var sum = Plus(address, false); + if (sum == null || sum.Value.IsNegative || sum.Value.Offset > UIntPtr.MaxValue) return null; return (UIntPtr)sum.Value.Offset; @@ -73,7 +73,7 @@ public readonly record struct PointerOffset(ulong Offset, bool IsNegative) /// The value of this offset as an address, or null if the offset is negative. public UIntPtr? AsAddress() { - if (IsNegative || Offset > (ulong)UIntPtr.MaxValue) + if (IsNegative || Offset > UIntPtr.MaxValue) return null; return (UIntPtr)Offset; } diff --git a/src/MindControl/Allocation/MemoryAllocation.cs b/src/MindControl/Allocation/MemoryAllocation.cs index dd21c5a..8dea42a 100644 --- a/src/MindControl/Allocation/MemoryAllocation.cs +++ b/src/MindControl/Allocation/MemoryAllocation.cs @@ -30,7 +30,7 @@ public class MemoryAllocation /// public bool IsDisposed { get; private set; } - private readonly ProcessMemory? _parentProcessMemory; + private readonly ProcessMemory _parentProcessMemory; /// /// Builds a new instance. @@ -52,7 +52,7 @@ internal MemoryAllocation(MemoryRange range, bool isExecutable, ProcessMemory pa public ulong GetTotalReservedSpace() { if (IsDisposed) - throw new ObjectDisposedException(nameof(MemoryAllocation)); + return 0; return Reservations.Aggregate(0, (current, subRange) => current + subRange?.Range.GetSize() ?? 0); @@ -67,7 +67,7 @@ public ulong GetTotalReservedSpace() public ulong GetRemainingSpace() { if (IsDisposed) - throw new ObjectDisposedException(nameof(MemoryAllocation)); + return 0; return Range.GetSize() - GetTotalReservedSpace(); } @@ -80,7 +80,7 @@ public ulong GetRemainingSpace() public MemoryRange? GetLargestReservableSpace() { if (IsDisposed) - throw new ObjectDisposedException(nameof(MemoryAllocation)); + return null; return GetFreeRanges().OrderByDescending(r => r.GetSize()) .Cast() @@ -119,7 +119,7 @@ private IEnumerable GetFreeRanges() public MemoryRange? GetNextRangeFittingSize(ulong size, uint? byteAlignment = 8) { if (IsDisposed) - throw new ObjectDisposedException(nameof(MemoryAllocation)); + return null; // Adjust the size to fit the alignment if (byteAlignment != null && size % byteAlignment.Value > 0) @@ -151,8 +151,10 @@ private IEnumerable GetFreeRanges() /// A result holding the resulting reservation or a reservation failure. public Result ReserveRange(ulong size, uint? byteAlignment = 8) { - if (IsDisposed) - throw new ObjectDisposedException(nameof(MemoryAllocation)); + if (IsDisposed || !_parentProcessMemory.IsAttached) + return new ReservationFailureOnDisposedAllocation(); + if (size == 0) + return new ReservationFailureOnInvalidArguments("The size to reserve must be more than 0 bytes."); var range = GetNextRangeFittingSize(size, byteAlignment); if (range == null) @@ -171,6 +173,9 @@ public Result ReserveRange(ulong size, ui /// The range of memory to free from reservations in this instance. public void FreeRange(MemoryRange rangeToFree) { + if (IsDisposed) + return; + for (int i = _reservations.Count - 1; i >= 0; i--) { var currentRange = _reservations[i]; @@ -231,6 +236,6 @@ public void Dispose() IsDisposed = true; ClearReservations(); - _parentProcessMemory?.Free(this); + _parentProcessMemory.Free(this); } } \ No newline at end of file diff --git a/src/MindControl/Internal/ConversionExtensions.cs b/src/MindControl/Internal/ConversionExtensions.cs index 0a621d4..24e1d93 100644 --- a/src/MindControl/Internal/ConversionExtensions.cs +++ b/src/MindControl/Internal/ConversionExtensions.cs @@ -1,5 +1,4 @@ -using System.Numerics; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using System.Text; namespace MindControl.Internal; @@ -52,24 +51,6 @@ public static byte[] ToBytes(this IntPtr pointer, bool is64Bit) Marshal.Copy(pointer, result, 0, result.Length); return result; } - - /// - /// Attempts to read an IntPtr from the given BigInteger value. - /// - /// Value to read as an IntPtr. - public static UIntPtr? ToUIntPtr(this BigInteger value) - { - if ((IntPtr.Size == 4 && value > uint.MaxValue) - || (IntPtr.Size == 8 && value > ulong.MaxValue) - || value < 0) - { - // Don't let arithmetic overflows occur. - // The input value is just not addressable. - return null; - } - - return IntPtr.Size == 4 ? (UIntPtr)(uint)value : (UIntPtr)(ulong)value; - } /// /// Converts the given structure to an array of bytes. @@ -87,6 +68,10 @@ private static byte[] StructToBytes(object value) var ptr = Marshal.AllocHGlobal(size); try { + // Copy the struct to the pointer and then into the array. + // Note that StructureToPtr may throw an AccessViolationException if the structure is not compatible + // (typically because it contains a reference type). This type of exception may cause crashes in certain + // cases, even when thrown in try/catch blocks. Marshal.StructureToPtr(value, ptr, true); Marshal.Copy(ptr, arr, 0, size); } diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs index 14873bf..e6f9e7a 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs @@ -15,6 +15,9 @@ public partial class ProcessMemory /// The memory address pointed by the pointer path. public Result EvaluateMemoryAddress(PointerPath pointerPath) { + if (!IsAttached) + return new PathEvaluationFailureOnDetachedProcess(); + if (pointerPath.IsStrictly64Bit && (IntPtr.Size == 4 || !Is64Bit)) return new PathEvaluationFailureOnIncompatibleBitness(); diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index 7f33a1c..70c35f2 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -27,6 +27,8 @@ public partial class ProcessMemory public Result Allocate(ulong size, bool forExecutableCode, MemoryRange? limitRange = null, UIntPtr? nearAddress = null) { + if (!IsAttached) + return new AllocationFailureOnDetachedProcess(); if (size == 0) return new AllocationFailureOnInvalidArguments( "The size of the memory range to allocate must be greater than zero."); @@ -174,7 +176,7 @@ private Result FindAndAllocateFreeMemory(ulong s } } - // If we reached the end of the memory range and didn't find a suitable free range. + // We reached the end of the memory range and didn't find a suitable free range. return new AllocationFailureOnNoFreeMemoryFound(actualRange.Value, nextAddress); } @@ -196,6 +198,8 @@ private Result FindAndAllocateFreeMemory(ulong s public Result Reserve(ulong size, bool requireExecutable, MemoryRange? limitRange = null, UIntPtr? nearAddress = null) { + if (!IsAttached) + return new AllocationFailureOnDetachedProcess(); if (size == 0) return new AllocationFailureOnInvalidArguments( "The size of the memory range to reserve must be greater than zero."); @@ -245,14 +249,26 @@ public Result Reserve(ulong size, bool req /// Data to store. /// Set to true if the data is executable code. Defaults to false. /// A result holding either the reserved memory range, or an allocation failure. - public Result Store(byte[] data, bool isCode = false) + public Result Store(byte[] data, bool isCode = false) { + if (!IsAttached) + return new StoreFailureOnDetachedProcess(); + if (data.Length == 0) + return new StoreFailureOnInvalidArguments("The data to store must not be empty."); + var reservedRangeResult = Reserve((ulong)data.Length, isCode); if (reservedRangeResult.IsFailure) - return reservedRangeResult.Error; + return new StoreFailureOnAllocation(reservedRangeResult.Error); var reservedRange = reservedRangeResult.Value; - WriteBytes(reservedRange.Range.Start, data, MemoryProtectionStrategy.Ignore); + var writeResult = WriteBytes(reservedRange.Range.Start, data, MemoryProtectionStrategy.Ignore); + if (writeResult.IsFailure) + { + // If writing the data failed, free the reservation and return the write failure. + reservedRange.Dispose(); + return new StoreFailureOnWrite(writeResult.Error); + } + return reservedRange; } @@ -265,15 +281,25 @@ public Result Store(byte[] data, bool isCo /// Data to store. /// Allocated memory to store the data. /// A result holding either the reservation storing the data, or a reservation failure. - public Result Store(byte[] data, MemoryAllocation allocation) + public Result Store(byte[] data, MemoryAllocation allocation) { + if (!IsAttached) + return new StoreFailureOnDetachedProcess(); + uint alignment = Is64Bit ? (uint)8 : 4; var reservedRangeResult = allocation.ReserveRange((ulong)data.Length, alignment); if (reservedRangeResult.IsFailure) - return reservedRangeResult.Error; + return new StoreFailureOnReservation(reservedRangeResult.Error); var reservedRange = reservedRangeResult.Value; - WriteBytes(reservedRange.Range.Start, data, MemoryProtectionStrategy.Ignore); + var writeResult = WriteBytes(reservedRange.Range.Start, data, MemoryProtectionStrategy.Ignore); + if (writeResult.IsFailure) + { + // If writing the data failed, free the reservation and return the write failure. + reservedRange.Dispose(); + return new StoreFailureOnWrite(writeResult.Error); + } + return reservedRange; } @@ -283,8 +309,9 @@ public Result Store(byte[] data, MemoryAl /// /// Value or structure to store. /// Type of the value or structure. - /// The reservation holding the data. - public Result Store(T value) + /// A result holding either the reservation where the data has been written, or an allocation failure. + /// + public Result Store(T value) => Store(value.ToBytes(), false); /// @@ -297,8 +324,9 @@ public Result Store(T value) /// Value or structure to store. /// Range of memory to store the data in. /// Type of the value or structure. - /// The reservation holding the data. - public Result Store(T value, MemoryAllocation allocation) where T: struct + /// A result holding either the reservation where the data has been written, or an allocation failure. + /// + public Result Store(T value, MemoryAllocation allocation) where T: struct => Store(value.ToBytes(), allocation); /// @@ -307,15 +335,18 @@ public Result Store(T value, MemoryAll /// /// String to store. /// String settings to use to write the string. - /// The reservation holding the string. - public Result StoreString(string value, StringSettings settings) + /// A result holding either the reservation where the string has been written, or an allocation failure. + /// + public Result StoreString(string value, StringSettings settings) { + if (!IsAttached) + return new StoreFailureOnDetachedProcess(); if (!settings.IsValid) - return new AllocationFailureOnInvalidArguments(StringSettings.InvalidSettingsMessage); + return new StoreFailureOnInvalidArguments(StringSettings.InvalidSettingsMessage); var bytes = settings.GetBytes(value); if (bytes == null) - return new AllocationFailureOnInvalidArguments(StringSettings.GetBytesFailureMessage); + return new StoreFailureOnInvalidArguments(StringSettings.GetBytesFailureMessage); return Store(bytes, isCode: false); } @@ -329,16 +360,19 @@ public Result StoreString(string value, St /// String to store. /// String settings to use to write the string. /// Range of memory to store the string in. - /// The reservation holding the string. - public Result StoreString(string value, StringSettings settings, + /// A result holding either the reservation where the string has been written, or an allocation failure. + /// + public Result StoreString(string value, StringSettings settings, MemoryAllocation allocation) { + if (!IsAttached) + return new StoreFailureOnDetachedProcess(); if (!settings.IsValid) - return new ReservationFailureOnInvalidArguments(StringSettings.InvalidSettingsMessage); + return new StoreFailureOnInvalidArguments(StringSettings.InvalidSettingsMessage); var bytes = settings.GetBytes(value); if (bytes == null) - return new ReservationFailureOnInvalidArguments(StringSettings.GetBytesFailureMessage); + return new StoreFailureOnInvalidArguments(StringSettings.GetBytesFailureMessage); return Store(bytes, allocation); } diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs b/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs index e59182d..c211a79 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs @@ -1,5 +1,4 @@ using System.Text; -using MindControl.Native; using MindControl.Results; namespace MindControl; @@ -27,7 +26,7 @@ public Result InjectLibrary(string libraryPath) public Result InjectLibrary(string libraryPath, TimeSpan waitTimeout) { if (!IsAttached) - throw new InvalidOperationException(DetachedErrorMessage); + return new InjectionFailureOnDetachedProcess(); // Check if the library file exists string absoluteLibraryPath = Path.GetFullPath(libraryPath); @@ -42,7 +41,7 @@ public Result InjectLibrary(string libraryPath, TimeSpan waitT // Store the library path string in the target process memory var reservationResult = StoreString(absoluteLibraryPath, new StringSettings(Encoding.Unicode)); if (reservationResult.IsFailure) - return new InjectionFailureOnParameterAllocation(reservationResult.Error); + return new InjectionFailureOnParameterStorage(reservationResult.Error); var reservation = reservationResult.Value; // Run LoadLibraryW from inside the target process to have it load the library itself, which is usually safer diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs index 151cabf..e225c52 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs @@ -25,6 +25,8 @@ public partial class ProcessMemory /// The value read from the process memory, or a read failure. public Result ReadBytes(PointerPath pointerPath, long length) { + if (!IsAttached) + return new ReadFailureOnDetachedProcess(); if (length < 0) return new ReadFailureOnInvalidArguments("The length to read cannot be negative."); @@ -39,6 +41,9 @@ public Result ReadBytes(PointerPath pointerPath, long lengt /// The value read from the process memory, or a read failure. public Result ReadBytes(PointerPath pointerPath, ulong length) { + if (!IsAttached) + return new ReadFailureOnDetachedProcess(); + var addressResult = EvaluateMemoryAddress(pointerPath); return addressResult.IsSuccess ? ReadBytes(addressResult.Value, length) : new ReadFailureOnPointerPathEvaluation(addressResult.Error); @@ -52,6 +57,8 @@ public Result ReadBytes(PointerPath pointerPath, ulong leng /// The value read from the process memory, or a read failure. public Result ReadBytes(UIntPtr address, long length) { + if (!IsAttached) + return new ReadFailureOnDetachedProcess(); if (length < 0) return new ReadFailureOnInvalidArguments("The length to read cannot be negative."); @@ -66,9 +73,10 @@ public Result ReadBytes(UIntPtr address, long length) /// The value read from the process memory, or a read failure. public Result ReadBytes(UIntPtr address, ulong length) { + if (!IsAttached) + return new ReadFailureOnDetachedProcess(); if (address == UIntPtr.Zero) return new ReadFailureOnZeroPointer(); - if (!IsBitnessCompatible(address)) return new ReadFailureOnIncompatibleBitness(address); @@ -90,6 +98,9 @@ public Result ReadBytes(UIntPtr address, ulong length) /// The value read from the process memory, or a read failure in case no bytes could be read. public Result ReadBytesPartial(PointerPath pointerPath, byte[] buffer, ulong maxLength) { + if (!IsAttached) + return new ReadFailureOnDetachedProcess(); + var addressResult = EvaluateMemoryAddress(pointerPath); return addressResult.IsSuccess ? ReadBytesPartial(addressResult.Value, buffer, maxLength) : new ReadFailureOnPointerPathEvaluation(addressResult.Error); @@ -108,6 +119,8 @@ public Result ReadBytesPartial(PointerPath pointerPath, byte /// The value read from the process memory, or a read failure in case no bytes could be read. public Result ReadBytesPartial(UIntPtr address, byte[] buffer, ulong maxLength) { + if (!IsAttached) + return new ReadFailureOnDetachedProcess(); if (maxLength == 0) return 0; if ((ulong)buffer.Length < maxLength) @@ -137,6 +150,9 @@ public Result ReadBytesPartial(UIntPtr address, byte[] buffe /// The value read from the process memory, or a read failure. public Result Read(PointerPath pointerPath) where T : struct { + if (!IsAttached) + return new ReadFailureOnDetachedProcess(); + var addressResult = EvaluateMemoryAddress(pointerPath); return addressResult.IsSuccess ? Read(addressResult.Value) : new ReadFailureOnPointerPathEvaluation(addressResult.Error); @@ -152,7 +168,8 @@ public Result Read(PointerPath pointerPath) where T : struct /// The value read from the process memory, or a read failure. public Result Read(UIntPtr address) where T : struct { - // Check the address + if (!IsAttached) + return new ReadFailureOnDetachedProcess(); if (address == UIntPtr.Zero) return new ReadFailureOnZeroPointer(); if (!IsBitnessCompatible(address)) @@ -161,7 +178,7 @@ public Result Read(UIntPtr address) where T : struct // Get the size of the target type int size; - // Exception for UIntPtr to use the size of the attached process platform, not the system platform + // Special case for UIntPtr to use the size of the attached process platform, not the system platform if (typeof(T) == typeof(UIntPtr)) size = Is64Bit ? 8 : 4; else @@ -185,7 +202,7 @@ public Result Read(UIntPtr address) where T : struct // Convert the bytes into the target type try { - // Exception for UIntPtr to use the size of the attached process platform, not the system platform + // Special case for UIntPtr to use the size of the attached process platform, not the system platform if (typeof(T) == typeof(UIntPtr) && !Is64Bit) return (T)(object)new UIntPtr(BitConverter.ToUInt32(bytes, 0)); @@ -208,6 +225,9 @@ public Result Read(UIntPtr address) where T : struct /// The value read from the process memory, or a read failure. public Result Read(Type type, PointerPath pointerPath) { + if (!IsAttached) + return new ReadFailureOnDetachedProcess(); + var addressResult = EvaluateMemoryAddress(pointerPath); return addressResult.IsSuccess ? Read(type, addressResult.Value) : new ReadFailureOnPointerPathEvaluation(addressResult.Error); @@ -223,11 +243,10 @@ public Result Read(Type type, PointerPath pointerPath) /// The value read from the process memory, or a read failure. public Result Read(Type type, UIntPtr address) { - // Check that the type is not a reference type + if (!IsAttached) + return new ReadFailureOnDetachedProcess(); if (!type.IsValueType) return new ReadFailureOnUnsupportedType(type); - - // Check the address if (address == UIntPtr.Zero) return new ReadFailureOnZeroPointer(); if (!IsBitnessCompatible(address)) @@ -275,24 +294,24 @@ public Result Read(Type type, UIntPtr address) /// Encodings to use when trying to find the encoding of a string, in order of priority. /// This list does not include ASCII, as there is no way to tell if a valid ASCII string is ASCII or UTF-8, /// and we choose to prioritize UTF-8. - private static readonly Encoding[] FindStringEncodings = { Encoding.UTF8, Encoding.Latin1, Encoding.Unicode }; + private static readonly Encoding[] FindStringEncodings = [Encoding.UTF8, Encoding.Latin1, Encoding.Unicode]; /// Length prefix sizes to try when trying to find the length prefix of a string, in order of priority. /// - private static readonly int[] FindStringLengthPrefixSizes = { 0, 4, 2, 1, 8 }; + private static readonly int[] FindStringLengthPrefixSizes = [0, 4, 2, 1, 8]; /// Length prefix sizes to try when trying to find the type prefix of a string, in order of priority. /// - private static readonly int[] FindStringTypePrefixSizes = { 0, 4, 8, 16 }; + private static readonly int[] FindStringTypePrefixSizes = [0, 4, 8, 16]; /// Length prefix units to try when trying to find the length prefix of a string, in order of priority. /// private static readonly StringLengthUnit[] FindStringLengthPrefixUnits = - { StringLengthUnit.Bytes, StringLengthUnit.Characters }; + [StringLengthUnit.Bytes, StringLengthUnit.Characters]; /// Null-termination settings to try when trying to find the null-termination of a string, in order of /// priority. - private static readonly bool[] FindStringNullTerminated = { true, false }; + private static readonly bool[] FindStringNullTerminated = [true, false]; /// /// Attempts to find appropriate settings to read and write strings at the pointer referred by the given path, @@ -309,6 +328,9 @@ public Result Read(Type type, UIntPtr address) public Result FindStringSettings(PointerPath pointerPath, string expectedString) { + if (!IsAttached) + return new FindStringSettingsFailureOnDetachedProcess(); + var addressResult = EvaluateMemoryAddress(pointerPath); return addressResult.IsSuccess ? FindStringSettings(addressResult.Value, expectedString) : new FindStringSettingsFailureOnPointerPathEvaluation(addressResult.Error); @@ -328,6 +350,8 @@ public Result FindStringSettings(Poin public Result FindStringSettings(UIntPtr stringPointerAddress, string expectedString) { + if (!IsAttached) + return new FindStringSettingsFailureOnDetachedProcess(); if (string.IsNullOrWhiteSpace(expectedString)) return new FindStringSettingsFailureOnNoSettingsFound(); @@ -469,6 +493,9 @@ public Result FindStringSettings(UInt public Result ReadRawString(PointerPath pointerPath, Encoding encoding, int? maxLength = null, bool isNullTerminated = true) { + if (!IsAttached) + return new ReadFailureOnDetachedProcess(); + var addressResult = EvaluateMemoryAddress(pointerPath); return addressResult.IsSuccess ? ReadRawString(addressResult.Value, encoding, maxLength, isNullTerminated) : new ReadFailureOnPointerPathEvaluation(addressResult.Error); @@ -494,6 +521,8 @@ public Result ReadRawString(PointerPath pointerPath, Encodi public Result ReadRawString(UIntPtr address, Encoding encoding, int? maxLength = null, bool isNullTerminated = true) { + if (!IsAttached) + return new ReadFailureOnDetachedProcess(); if (maxLength is < 0) return new ReadFailureOnInvalidArguments("The maximum length cannot be negative."); if (maxLength == 0) @@ -540,6 +569,9 @@ public Result ReadRawString(UIntPtr address, Encoding encod /// The string read from the process memory, or a read failure. public Result ReadStringPointer(PointerPath pointerPath, StringSettings settings) { + if (!IsAttached) + return new StringReadFailureOnDetachedProcess(); + var addressResult = EvaluateMemoryAddress(pointerPath); return addressResult.IsSuccess ? ReadStringPointer(addressResult.Value, settings) : new StringReadFailureOnPointerPathEvaluation(addressResult.Error); @@ -556,6 +588,8 @@ public Result ReadStringPointer(PointerPath pointerPa /// The string read from the process memory, or a read failure. public Result ReadStringPointer(UIntPtr address, StringSettings settings) { + if (!IsAttached) + return new StringReadFailureOnDetachedProcess(); if (!IsBitnessCompatible(address)) return new StringReadFailureOnIncompatibleBitness(address); if (address == UIntPtr.Zero) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Thread.cs b/src/MindControl/ProcessMemory/ProcessMemory.Threading.cs similarity index 51% rename from src/MindControl/ProcessMemory/ProcessMemory.Thread.cs rename to src/MindControl/ProcessMemory/ProcessMemory.Threading.cs index 2323b2e..a548d98 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Thread.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Threading.cs @@ -13,16 +13,19 @@ public partial class ProcessMemory /// wait for the thread to finish execution. Use the resulting instance to wait for the /// thread to finish if you need to. /// - /// Address of the function (or start instruction) to run in the target process. + /// Address of the function (starting instruction) to run in the target process. /// - /// An optional parameter to pass to the function. It can be either an address or a value, - /// depending on the function parameters. If the function takes multiple or complex parameters, consider using - /// and/or using a structure holding each parameter - /// with a matching field order. + /// An optional parameter to pass to the function. It can be either an address or a value. + /// This input value will be stored in register RDX for x64, or EBX for x86. If this does not match the call + /// conventions of the target function, the thread must execute a "trampoline" code that arranges the parameter in + /// the expected way before calling the function. See the documentation for more info. /// A result holding either the thread instance that you can use to wait for the thread to return, or a /// error. public Result RunThread(UIntPtr functionAddress, UIntPtr? parameter = null) { + if (!IsAttached) + return new ThreadFailureOnDetachedProcess(); + var startResult = StartThread(functionAddress, parameter ?? UIntPtr.Zero); if (startResult.IsFailure) return startResult.Error; @@ -36,17 +39,21 @@ public Result RunThread(UIntPtr functionAddress, UI /// /// Pointer path to the function (or start instruction) to run in the target /// process. - /// An optional parameter to pass to the function. It can be either an address or a value, - /// depending on the function parameters. If the function takes multiple or complex parameters, consider using - /// and/or using a structure holding each parameter - /// with a matching field order. + /// An optional parameter to pass to the function. It can be either an address or a value. + /// This input value will be stored in register RDX for x64, or EBX for x86. If this does not match the call + /// conventions of the target function, the thread must execute a "trampoline" code that arranges the parameter in + /// the expected way before calling the function. See the documentation for more info. /// A result holding either the thread instance that you can use to wait for the thread to return, or a /// error. public Result RunThread(PointerPath functionPointerPath, UIntPtr? parameter = null) { + if (!IsAttached) + return new ThreadFailureOnDetachedProcess(); + var evaluateResult = EvaluateMemoryAddress(functionPointerPath); if (evaluateResult.IsFailure) return new ThreadFailureOnPointerPathEvaluation(evaluateResult.Error); + return RunThread(evaluateResult.Value, parameter); } @@ -57,88 +64,23 @@ public Result RunThread(PointerPath functionPointer /// /// Name of the module containing the function to run (e.g. "kernel32.dll"). /// Name of the exported function to run from the specified module. - /// An optional parameter to pass to the function. It can be either an address or a value, - /// depending on the function parameters. If the function takes multiple or complex parameters, consider using - /// and/or using a structure holding each parameter - /// with a matching field order. + /// An optional parameter to pass to the function. It can be either an address or a value. + /// This input value will be stored in register RDX for x64, or EBX for x86. If this does not match the call + /// conventions of the target function, the thread must execute a "trampoline" code that arranges the parameter in + /// the expected way before calling the function. See the documentation for more info. /// A result holding either the thread instance that you can use to wait for the thread to return, or a /// error. public Result RunThread(string moduleName, string functionName, UIntPtr? parameter = null) { - var functionAddressResult = FindFunctionAddress(moduleName, functionName); - if (functionAddressResult.IsFailure) - return functionAddressResult.Error; + if (!IsAttached) + return new ThreadFailureOnDetachedProcess(); - return RunThread(functionAddressResult.Value, parameter); - } - - /// - /// Stores the given parameter in the target process memory, and then starts a new thread in the target process, - /// running the function at the specified address. This method will not wait for the thread to finish execution. - /// Use the resulting instance to wait for the thread to finish if you need to. - /// - /// Address of the function (or start instruction) to run in the target process. - /// - /// Parameter to pass to the function. If the function takes multiple parameters, they can - /// be arranged in a structure holding each value in the right field order. - /// A result holding either the thread instance that you can use to wait for the thread to return, or a - /// error. - public Result RunThreadWithStoredParameter(UIntPtr functionAddress, - T parameter) - { - var storeResult = Store(parameter); - if (storeResult.IsFailure) - return new ThreadFailureOnParameterStorageFailure(storeResult.Error); - - var reservation = storeResult.Value; - var startResult = StartThread(functionAddress, reservation.Address); - if (startResult.IsFailure) - return startResult.Error; - return new ParameterizedRemoteThread(_osService, startResult.Value, reservation); - } - - /// - /// Stores the given parameter in the target process memory, and then starts a new thread in the target process, - /// running the function at the address pointed by the specified pointer path. This method will not wait for the - /// thread to finish execution. Use the resulting instance to wait for the thread to - /// finish if you need to. - /// - /// Pointer path to the function (or start instruction) to run in the target - /// process. - /// Parameter to pass to the function. If the function takes multiple parameters, they can - /// be arranged in a structure holding each value in the right field order. - /// A result holding either the thread instance that you can use to wait for the thread to return, or a - /// error. - public Result RunThreadWithStoredParameter( - PointerPath functionPointerPath, T parameter) - { - var evaluateResult = EvaluateMemoryAddress(functionPointerPath); - if (evaluateResult.IsFailure) - return new ThreadFailureOnPointerPathEvaluation(evaluateResult.Error); - return RunThreadWithStoredParameter(evaluateResult.Value, parameter); - } - - /// - /// Stores the given parameter in the target process memory, and then starts a new thread in the target process, - /// running the specified exported function from a module loaded into the target process. This method will not wait - /// for the thread to finish execution. Use the resulting instance to wait for the - /// thread to finish if you need to. - /// - /// Name of the module containing the function to run (e.g. "kernel32.dll"). - /// Name of the exported function to run from the specified module. - /// Parameter to pass to the function. If the function takes multiple parameters, they can - /// be arranged in a structure holding each value in the right field order. - /// A result holding either the thread instance that you can use to wait for the thread to return, or a - /// error. - public Result RunThreadWithStoredParameter(string moduleName, - string functionName, T parameter) - { var functionAddressResult = FindFunctionAddress(moduleName, functionName); if (functionAddressResult.IsFailure) return functionAddressResult.Error; - return RunThreadWithStoredParameter(functionAddressResult.Value, parameter); + return RunThread(functionAddressResult.Value, parameter); } #endregion @@ -155,6 +97,15 @@ public Result RunThreadWithStoredParam /// A result holding either the handle to the thread, or a error. private Result StartThread(UIntPtr functionAddress, UIntPtr parameter) { + if (functionAddress == UIntPtr.Zero) + return new ThreadFailureOnInvalidArguments("The function address cannot be zero."); + if (!Is64Bit && functionAddress.ToUInt64() > uint.MaxValue) + return new ThreadFailureOnInvalidArguments( + $"The function address exceeds the maximum value for 32-bit processes ({uint.MaxValue})."); + if (!Is64Bit && parameter.ToUInt64() > uint.MaxValue) + return new ThreadFailureOnInvalidArguments( + $"The provided parameter exceeds the maximum value for 32-bit processes ({uint.MaxValue})."); + var remoteThreadResult = _osService.CreateRemoteThread(ProcessHandle, functionAddress, parameter); if (remoteThreadResult.IsFailure) return new ThreadFailureOnSystemFailure("Failed to create the thread.", remoteThreadResult.Error); diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs index 0a52256..4b868c6 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs @@ -20,6 +20,9 @@ public partial class ProcessMemory public Result WriteBytes(PointerPath path, byte[] value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { + if (!IsAttached) + return new WriteFailureOnDetachedProcess(); + var addressResult = EvaluateMemoryAddress(path); return addressResult.IsSuccess ? WriteBytes(addressResult.Value, value, memoryProtectionStrategy) : new WriteFailureOnPointerPathEvaluation(addressResult.Error); @@ -36,6 +39,13 @@ public Result WriteBytes(PointerPath path, byte[] value, public Result WriteBytes(UIntPtr address, Span value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { + if (!IsAttached) + return new WriteFailureOnDetachedProcess(); + if (address == UIntPtr.Zero) + return new WriteFailureOnZeroPointer(); + if (!IsBitnessCompatible(address)) + return new WriteFailureOnIncompatibleBitness(address); + // Remove protection if needed memoryProtectionStrategy ??= DefaultWriteStrategy; MemoryProtection? previousProtection = null; @@ -86,6 +96,8 @@ public Result WriteBytes(UIntPtr address, Span value, public Result Write(PointerPath path, T value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { + if (!IsAttached) + return new WriteFailureOnDetachedProcess(); var addressResult = EvaluateMemoryAddress(path); return addressResult.IsSuccess ? Write(addressResult.Value, value, memoryProtectionStrategy) : new WriteFailureOnPointerPathEvaluation(addressResult.Error); @@ -99,13 +111,18 @@ public Result Write(PointerPath path, T value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// Type of the value to write. - /// Thrown when the type of the value is not supported. /// A successful result, or a write failure public Result Write(UIntPtr address, T value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { + if (!IsAttached) + return new WriteFailureOnDetachedProcess(); + if (address == UIntPtr.Zero) + return new WriteFailureOnZeroPointer(); + if (!IsBitnessCompatible(address)) + return new WriteFailureOnIncompatibleBitness(address); if (value == null) - throw new ArgumentNullException(nameof(value), "The value to write cannot be null."); + return new WriteFailureOnInvalidArguments("The value to write cannot be null."); if (value is IntPtr ptr && ptr.ToInt64() > uint.MaxValue) return new WriteFailureOnIncompatibleBitness((UIntPtr)ptr); if (value is UIntPtr uptr && uptr.ToUInt64() > uint.MaxValue) @@ -121,16 +138,48 @@ public Result Write(UIntPtr address, T value, uint v => WriteUInt(address, v, memoryProtectionStrategy), IntPtr v => WriteIntPtr(address, v, memoryProtectionStrategy), UIntPtr v => Is64Bit ? WriteULong(address, v.ToUInt64(), memoryProtectionStrategy) - : WriteUInt(address, (uint)v.ToUInt64(), memoryProtectionStrategy), + : WriteUInt(address, v.ToUInt32(), memoryProtectionStrategy), float v => WriteFloat(address, v, memoryProtectionStrategy), long v => WriteLong(address, v, memoryProtectionStrategy), ulong v => WriteULong(address, v, memoryProtectionStrategy), double v => WriteDouble(address, v, memoryProtectionStrategy), byte[] v => WriteBytes(address, v, memoryProtectionStrategy), - _ => WriteBytes(address, value.ToBytes(), memoryProtectionStrategy) + _ => ConvertAndWriteBytes(address, value, memoryProtectionStrategy) }; } - + + /// + /// Converts the given value to an array of bytes, then writes it to the given address in the process memory. + /// This method is used when the type of the value to write is not supported by the other write methods (typically + /// for structs). + /// + /// Target address in the process memory. + /// Value to write. + /// Strategy to use to deal with memory protection. If null (default), the + /// of this instance is used. + /// Type of the value to write. + /// A successful result, or a write failure + private Result ConvertAndWriteBytes(UIntPtr address, T value, + MemoryProtectionStrategy? memoryProtectionStrategy = null) + { + if (value == null) + return new WriteFailureOnInvalidArguments("The value to write cannot be null."); + if (value is not ValueType) + return new WriteFailureOnUnsupportedType(value.GetType()); + + byte[] bytes; + try + { + bytes = value.ToBytes(); + } + catch (Exception e) + { + return new WriteFailureOnConversion(typeof(T), e); + } + + return WriteBytes(address, bytes, memoryProtectionStrategy); + } + /// /// Writes a boolean value to the given address in the process memory. /// diff --git a/src/MindControl/ProcessMemory/ProcessMemory.cs b/src/MindControl/ProcessMemory/ProcessMemory.cs index 96fdb94..89ae8bc 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.cs @@ -12,10 +12,6 @@ namespace MindControl; /// public partial class ProcessMemory : IDisposable { - /// Exception message to use in exceptions thrown when trying to use a detached process. - private const string DetachedErrorMessage = - "The process is not attached. It may have exited or this instance may have been disposed."; - private readonly Process _process; private readonly IOperatingSystemService _osService; private readonly bool _ownsProcessInstance; diff --git a/src/MindControl/Results/AllocationFailure.cs b/src/MindControl/Results/AllocationFailure.cs index 2306d20..81814bb 100644 --- a/src/MindControl/Results/AllocationFailure.cs +++ b/src/MindControl/Results/AllocationFailure.cs @@ -1,24 +1,17 @@ namespace MindControl.Results; -/// -/// Represents a reason for a memory allocation operation to fail. -/// +/// Represents a reason for a memory allocation operation to fail. public enum AllocationFailureReason { - /// - /// The arguments provided to the allocation operation are invalid. - /// + /// The target process is not attached. + DetachedProcess, + /// The arguments provided to the allocation operation are invalid. InvalidArguments, - - /// - /// The provided limit range is not within the bounds of the target process applicative memory range. + /// The provided limit range is not within the bounds of the target process applicative memory range. /// LimitRangeOutOfBounds, - - /// - /// No free memory was found in the target process that would be large enough to accomodate the specified size - /// within the searched range. - /// + /// No free memory was found in the target process that would be large enough to accomodate the specified + /// size within the searched range. NoFreeMemoryFound } @@ -28,6 +21,17 @@ public enum AllocationFailureReason /// Reason for the failure. public abstract record AllocationFailure(AllocationFailureReason Reason); +/// +/// Represents a failure in a memory allocation operation when the target process is not attached. +/// +public record AllocationFailureOnDetachedProcess() + : AllocationFailure(AllocationFailureReason.DetachedProcess) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => Failure.DetachedErrorMessage; +} + /// /// Represents a failure in a memory allocation operation when the provided arguments are invalid. /// diff --git a/src/MindControl/Results/Failure.cs b/src/MindControl/Results/Failure.cs new file mode 100644 index 0000000..8fa677b --- /dev/null +++ b/src/MindControl/Results/Failure.cs @@ -0,0 +1,11 @@ +namespace MindControl.Results; + +/// +/// Provides members to be used in error results. +/// +public static class Failure +{ + /// Message used in error results when the process is detached. + public const string DetachedErrorMessage = + "The process is not attached. It may have exited or the process memory instance may have been disposed."; +} \ No newline at end of file diff --git a/src/MindControl/Results/FindStringSettingsFailure.cs b/src/MindControl/Results/FindStringSettingsFailure.cs index 11de3ae..ed70df2 100644 --- a/src/MindControl/Results/FindStringSettingsFailure.cs +++ b/src/MindControl/Results/FindStringSettingsFailure.cs @@ -1,33 +1,19 @@ namespace MindControl.Results; -/// -/// Represents a reason for a string settings search operation to fail. -/// +/// Represents a reason for a string settings search operation to fail. public enum FindStringSettingsFailureReason { - /// - /// Failure when trying to evaluate the given pointer path. - /// + /// The target process is not attached. + DetachedProcess, + /// Failure when trying to evaluate the given pointer path. PointerPathEvaluation, - - /// - /// Failure when trying to read the given pointer. - /// + /// Failure when trying to read the given pointer. PointerReadFailure, - - /// - /// The given pointer is a zero pointer. - /// + /// The given pointer is a zero pointer. ZeroPointer, - - /// - /// Failure when trying to read bytes at the address pointed by the given pointer. - /// + /// Failure when trying to read bytes at the address pointed by the given pointer. StringReadFailure, - - /// - /// No adequate settings were found to read the given string from the specified pointer. - /// + /// No adequate settings were found to read the given string from the specified pointer. NoSettingsFound } @@ -37,6 +23,17 @@ public enum FindStringSettingsFailureReason /// Reason for the failure. public abstract record FindStringSettingsFailure(FindStringSettingsFailureReason Reason); +/// +/// Represents a failure in a string settings search operation when the target process is not attached. +/// +public record FindStringSettingsFailureOnDetachedProcess() + : FindStringSettingsFailure(FindStringSettingsFailureReason.DetachedProcess) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => Failure.DetachedErrorMessage; +} + /// /// Represents a failure in a string settings search operation when failing to evaluate the specified pointer path. /// diff --git a/src/MindControl/Results/InjectionFailure.cs b/src/MindControl/Results/InjectionFailure.cs index 5faada0..275971a 100644 --- a/src/MindControl/Results/InjectionFailure.cs +++ b/src/MindControl/Results/InjectionFailure.cs @@ -1,16 +1,16 @@ namespace MindControl.Results; -/// -/// Represents a reason for an injection operation to fail. -/// +/// Represents a reason for an injection operation to fail. public enum InjectionFailureReason { + /// The target process is not attached. + DetachedProcess, /// The library file to inject was not found. LibraryFileNotFound, /// The module to inject is already loaded in the target process. ModuleAlreadyLoaded, - /// Failure when trying to reserve memory to store function parameters. - ParameterAllocationFailure, + /// Failure when trying to store function parameters. + ParameterStorageFailure, /// Failure when running the library loading thread. ThreadFailure, /// The library failed to load. @@ -22,6 +22,17 @@ public enum InjectionFailureReason /// public abstract record InjectionFailure(InjectionFailureReason Reason); +/// +/// Represents a failure in an injection operation when the target process is not attached. +/// +public record InjectionFailureOnDetachedProcess() + : InjectionFailure(InjectionFailureReason.DetachedProcess) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => Failure.DetachedErrorMessage; +} + /// /// Represents a failure in an injection operation when the library file to inject was not found. /// @@ -48,16 +59,16 @@ public override string ToString() } /// -/// Represents a failure in an injection operation when trying to reserve memory to store function parameters. +/// Represents a failure in an injection operation when trying to store function parameters. /// /// Details about the failure. -public record InjectionFailureOnParameterAllocation(AllocationFailure Details) - : InjectionFailure(InjectionFailureReason.ParameterAllocationFailure) +public record InjectionFailureOnParameterStorage(StoreFailure Details) + : InjectionFailure(InjectionFailureReason.ParameterStorageFailure) { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"Failed to allocate memory to store function parameters required to inject the library: {Details}"; + => $"Failed to store function parameters required to inject the library: {Details}"; } /// diff --git a/src/MindControl/Results/PathEvaluationFailure.cs b/src/MindControl/Results/PathEvaluationFailure.cs index 1e3433d..90106e2 100644 --- a/src/MindControl/Results/PathEvaluationFailure.cs +++ b/src/MindControl/Results/PathEvaluationFailure.cs @@ -1,28 +1,17 @@ namespace MindControl.Results; -/// -/// Represents a reason for a path evaluation operation to fail. -/// +/// Represents a reason for a path evaluation operation to fail. public enum PathEvaluationFailureReason { - /// - /// The target process is 32-bit, but the path is not compatible with a 32-bit address space. - /// + /// The target process is not attached. + DetachedProcess, + /// The target process is 32-bit, but the path is not compatible with a 32-bit address space. IncompatibleBitness, - - /// - /// The module specified in the pointer path was not found. - /// + /// The module specified in the pointer path was not found. BaseModuleNotFound, - - /// - /// A pointer in the path is a zero pointer or otherwise out of the target process address space. - /// + /// A pointer in the path is a zero pointer or otherwise out of the target process address space. PointerOutOfRange, - - /// - /// Failure when attempting to read a pointer from the path. - /// + /// Failure when attempting to read a pointer from the path. PointerReadFailure } @@ -32,6 +21,17 @@ public enum PathEvaluationFailureReason /// Reason for the failure. public abstract record PathEvaluationFailure(PathEvaluationFailureReason Reason); +/// +/// Represents a failure in a path evaluation operation when the target process is not attached. +/// +public record PathEvaluationFailureOnDetachedProcess() + : PathEvaluationFailure(PathEvaluationFailureReason.DetachedProcess) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => Failure.DetachedErrorMessage; +} + /// /// Represents a failure in a path evaluation operation when the target process is 32-bit, but the target memory /// address is not within the 32-bit address space. diff --git a/src/MindControl/Results/ReadFailure.cs b/src/MindControl/Results/ReadFailure.cs index cab9356..39ffab4 100644 --- a/src/MindControl/Results/ReadFailure.cs +++ b/src/MindControl/Results/ReadFailure.cs @@ -1,43 +1,24 @@ namespace MindControl.Results; -/// -/// Represents a reason for a memory read operation to fail. -/// +/// Represents a reason for a memory read operation to fail. public enum ReadFailureReason { - /// - /// The arguments provided to the memory read operation are invalid. - /// + /// The target process is not attached. + DetachedProcess, + /// The arguments provided to the memory read operation are invalid. InvalidArguments, - - /// - /// Failure when evaluating the pointer path to the target address. - /// + /// Failure when evaluating the pointer path to the target address. PointerPathEvaluationFailure, - - /// - /// The target process is 32-bit, but the target memory address is not within the 32-bit address space. + /// The target process is 32-bit, but the target memory address is not within the 32-bit address space. /// IncompatibleBitness, - - /// - /// The target pointer is a zero pointer. - /// + /// The target pointer is a zero pointer. ZeroPointer, - - /// - /// Failure when invoking the system API to read the target memory. - /// + /// Failure when invoking the system API to read the target memory. SystemReadFailure, - - /// - /// Failure when trying to convert the bytes read from memory to the target type. - /// + /// Failure when trying to convert the bytes read from memory to the target type. ConversionFailure, - - /// - /// The type to read is not supported. - /// + /// The type to read is not supported. UnsupportedType } @@ -47,6 +28,17 @@ public enum ReadFailureReason /// Reason for the failure. public abstract record ReadFailure(ReadFailureReason Reason); +/// +/// Represents a failure in a memory read operation when the target process is not attached. +/// +public record ReadFailureOnDetachedProcess() + : ReadFailure(ReadFailureReason.DetachedProcess) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => Failure.DetachedErrorMessage; +} + /// /// Represents a failure in a memory read operation when the arguments provided are invalid. /// diff --git a/src/MindControl/Results/ReservationFailure.cs b/src/MindControl/Results/ReservationFailure.cs index f0233b6..1a10735 100644 --- a/src/MindControl/Results/ReservationFailure.cs +++ b/src/MindControl/Results/ReservationFailure.cs @@ -1,18 +1,13 @@ namespace MindControl.Results; -/// -/// Represents a reason for a memory reservation operation to fail. -/// +/// Represents a reason for a memory reservation operation to fail. public enum ReservationFailureReason { - /// - /// The arguments provided to the reservation operation are invalid. - /// + /// The target allocation has been disposed. + DisposedAllocation, + /// The arguments provided to the reservation operation are invalid. InvalidArguments, - - /// - /// No space is available within the allocated memory range to reserve the specified size. - /// + /// No space is available within the allocated memory range to reserve the specified size. NoSpaceAvailable } @@ -22,6 +17,17 @@ public enum ReservationFailureReason /// Reason for the failure. public abstract record ReservationFailure(ReservationFailureReason Reason); +/// +/// Represents a failure in a memory reservation operation when the target allocation has been disposed. +/// +public record ReservationFailureOnDisposedAllocation() + : ReservationFailure(ReservationFailureReason.DisposedAllocation) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => "The target allocation has been disposed."; +} + /// /// Represents a failure in a memory reservation operation when the provided arguments are invalid. /// diff --git a/src/MindControl/Results/StoreFailure.cs b/src/MindControl/Results/StoreFailure.cs new file mode 100644 index 0000000..9a4a40f --- /dev/null +++ b/src/MindControl/Results/StoreFailure.cs @@ -0,0 +1,69 @@ +namespace MindControl.Results; + +/// Represents a reason for a store operation to fail. +public enum StoreFailureReason +{ + /// The target process is not attached. + DetachedProcess, + /// The arguments provided to the store operation are invalid. + InvalidArguments, + /// The allocation operation failed. + AllocationFailure, + /// The reservation operation failed. + ReservationFailure, + /// The write operation failed. + WriteFailure +} + +/// Represents a failure in a memory store operation. +/// Reason for the failure. +public abstract record StoreFailure(StoreFailureReason Reason); + +/// Represents a failure in a memory store operation when the target process is not attached. +public record StoreFailureOnDetachedProcess() + : StoreFailure(StoreFailureReason.DetachedProcess) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => Failure.DetachedErrorMessage; +} + +/// Represents a failure in a memory store operation when the provided arguments are invalid. +/// Message that describes how the arguments fail to meet expectations. +public record StoreFailureOnInvalidArguments(string Message) + : StoreFailure(StoreFailureReason.InvalidArguments) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The arguments provided are invalid: {Message}"; +} + +/// Represents a failure in a memory store operation when the allocation operation failed. +/// The allocation failure that caused the store operation to fail. +public record StoreFailureOnAllocation(AllocationFailure Details) + : StoreFailure(StoreFailureReason.AllocationFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The allocation operation failed: {Details}"; +} + +/// Represents a failure in a memory store operation when the reservation operation failed. +/// The reservation failure that caused the store operation to fail. +public record StoreFailureOnReservation(ReservationFailure Details) + : StoreFailure(StoreFailureReason.ReservationFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The reservation operation failed: {Details}"; +} + +/// Represents a failure in a memory store operation when the write operation failed. +/// The write failure that caused the store operation to fail. +public record StoreFailureOnWrite(WriteFailure Details) + : StoreFailure(StoreFailureReason.WriteFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The write operation failed: {Details}"; +} \ No newline at end of file diff --git a/src/MindControl/Results/StringReadFailure.cs b/src/MindControl/Results/StringReadFailure.cs index cd78b8d..51b132a 100644 --- a/src/MindControl/Results/StringReadFailure.cs +++ b/src/MindControl/Results/StringReadFailure.cs @@ -1,45 +1,25 @@ namespace MindControl.Results; -/// -/// Represents a reason for a string read operation to fail. -/// +/// Represents a reason for a string read operation to fail. public enum StringReadFailureReason { - /// - /// Failure when evaluating the pointer path to the target address. - /// + /// The target process is not attached. + DetachedProcess, + /// Failure when evaluating the pointer path to the target address. PointerPathEvaluationFailure, - - /// - /// The string read operation failed because the settings provided are invalid. - /// + /// The string read operation failed because the settings provided are invalid. InvalidSettings, - - /// - /// The string read operation failed because the target process is 32-bit, but the target memory address is not - /// within the 32-bit address space. - /// + /// The string read operation failed because the target process is 32-bit, but the target memory address is + /// not within the 32-bit address space. IncompatibleBitness, - - /// - /// The string read operation failed because the target pointer is a zero pointer. - /// + /// The string read operation failed because the target pointer is a zero pointer. ZeroPointer, - - /// - /// The pointer read operation failed. - /// + /// The pointer read operation failed. PointerReadFailure, - - /// - /// A read operation failed when attempting to read bytes from the actual string. - /// + /// A read operation failed when attempting to read bytes from the actual string. StringBytesReadFailure, - - /// - /// The length prefix of the string was evaluated to a value exceeding the configured max length, or a null - /// terminator was not found within the configured max length. - /// + /// The length prefix of the string was evaluated to a value exceeding the configured max length, or a null + /// terminator was not found within the configured max length. StringTooLong } @@ -49,6 +29,17 @@ public enum StringReadFailureReason /// Reason for the failure. public abstract record StringReadFailure(StringReadFailureReason Reason); +/// +/// Represents a failure in a string read operation when the target process is not attached. +/// +public record StringReadFailureOnDetachedProcess() + : StringReadFailure(StringReadFailureReason.DetachedProcess) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => Failure.DetachedErrorMessage; +} + /// /// Represents a failure in a string read operation when the pointer path evaluation failed. /// diff --git a/src/MindControl/Results/ThreadFailure.cs b/src/MindControl/Results/ThreadFailure.cs index 4648bde..a4847fb 100644 --- a/src/MindControl/Results/ThreadFailure.cs +++ b/src/MindControl/Results/ThreadFailure.cs @@ -1,16 +1,14 @@ namespace MindControl.Results; -/// -/// Represents a reason for a thread operation to fail. -/// +/// Represents a reason for a thread operation to fail. public enum ThreadFailureReason { + /// The target process is not attached. + DetachedProcess, /// Invalid arguments were provided to the thread operation. InvalidArguments, /// The thread handle has already been disposed. DisposedInstance, - /// Failure when storing a thread parameter in the target process memory. - ParameterStorageFailure, /// Failure when evaluating the pointer path to the target address. PointerPathEvaluationFailure, /// The target function cannot be found. @@ -28,6 +26,17 @@ public enum ThreadFailureReason /// public record ThreadFailure(ThreadFailureReason Reason); +/// +/// Represents a failure in a thread operation when the target process is not attached. +/// +public record ThreadFailureOnDetachedProcess() + : ThreadFailure(ThreadFailureReason.DetachedProcess) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => Failure.DetachedErrorMessage; +} + /// /// Represents a failure in a thread operation when the arguments provided are invalid. /// @@ -52,19 +61,6 @@ public record ThreadFailureOnDisposedInstance() public override string ToString() => "The thread handle has already been disposed."; } -/// -/// Represents a failure in a thread operation when storing a thread parameter in the target process memory. -/// -/// Details about the failure. -public record ThreadFailureOnParameterStorageFailure(AllocationFailure Details) - : ThreadFailure(ThreadFailureReason.ParameterStorageFailure) -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"Could not store the thread parameter in the target process memory: {Details}"; -} - /// /// Represents a failure in a thread operation when evaluating the pointer path to the target address. /// @@ -98,7 +94,7 @@ public record ThreadFailureOnWaitTimeout() /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => $"The thread did not finish execution within the specified timeout."; + => "The thread did not finish execution within the specified timeout."; } /// diff --git a/src/MindControl/Results/WriteFailure.cs b/src/MindControl/Results/WriteFailure.cs index 3b22be6..2d872eb 100644 --- a/src/MindControl/Results/WriteFailure.cs +++ b/src/MindControl/Results/WriteFailure.cs @@ -5,40 +5,28 @@ /// public enum WriteFailureReason { - /// - /// Failure when evaluating the pointer path to the target memory. - /// + /// The target process is not attached. + DetachedProcess, + /// Failure when evaluating the pointer path to the target memory. PointerPathEvaluationFailure, - - /// - /// The target process is 32-bit, but the target memory address is not within the 32-bit address space. + /// The arguments provided to the memory read operation are invalid. + InvalidArguments, + /// The type to write is not supported. + UnsupportedType, + /// The target process is 32-bit, but the target memory address is not within the 32-bit address space. /// IncompatibleBitness, - - /// - /// The target address is a zero pointer. - /// + /// The target address is a zero pointer. ZeroPointer, - - /// - /// Failure when invoking the system API to remove the protection properties of the target memory space. + /// Failure when invoking the system API to remove the protection properties of the target memory space. /// SystemProtectionRemovalFailure, - - /// - /// Failure when invoking the system API to restore the protection properties of the target memory space after - /// writing. - /// + /// Failure when invoking the system API to restore the protection properties of the target memory space + /// after writing. SystemProtectionRestorationFailure, - - /// - /// Failure when invoking the system API to write bytes in memory. - /// + /// Failure when invoking the system API to write bytes in memory. SystemWriteFailure, - - /// - /// Failure when trying to convert the value to write to an array of bytes to write in memory. - /// + /// Failure when trying to convert the value to write to an array of bytes to write in memory. ConversionFailure } @@ -48,6 +36,17 @@ public enum WriteFailureReason /// Reason for the failure. public abstract record WriteFailure(WriteFailureReason Reason); +/// +/// Represents a failure in a memory write operation when the target process is not attached. +/// +public record WriteFailureOnDetachedProcess() + : WriteFailure(WriteFailureReason.DetachedProcess) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => Failure.DetachedErrorMessage; +} + /// /// Represents a failure in a memory write operation when evaluating the pointer path to the target memory. /// @@ -61,6 +60,30 @@ public override string ToString() => $"Failed to evaluate the specified pointer path: {Details}"; } +/// +/// Represents a failure in a memory write operation when the arguments provided are invalid. +/// +/// Message that describes how the arguments fail to meet expectations. +public record WriteFailureOnInvalidArguments(string Message) + : WriteFailure(WriteFailureReason.InvalidArguments) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The arguments provided are invalid: {Message}"; +} + +/// +/// Represents a failure in a memory write operation when the value to write cannot be converted to an array of bytes. +/// +/// Type that caused the failure. +public record WriteFailureOnUnsupportedType(Type Type) + : WriteFailure(WriteFailureReason.UnsupportedType) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"The type {Type} is not supported for writing."; +} + /// /// Represents a failure in a memory write operation when the target process is 32-bit, but the target memory address /// is not within the 32-bit address space. @@ -129,3 +152,18 @@ public record WriteFailureOnSystemWrite(UIntPtr Address, SystemFailure Details) public override string ToString() => $"Failed to write at the address {Address}: {Details}"; } + +/// +/// Represents a failure in a memory write operation when trying to convert the value to write to an array of bytes to +/// write in memory. +/// +/// Type that caused the failure. +/// Exception that occurred during the conversion. +public record WriteFailureOnConversion(Type Type, Exception ConversionException) + : WriteFailure(WriteFailureReason.ConversionFailure) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"Failed to convert the value of type {Type} to an array of bytes. Make sure the type has a fixed length. See the ConversionException property for more details."; +} diff --git a/src/MindControl/Threading/ParameterizedRemoteThread.cs b/src/MindControl/Threading/ParameterizedRemoteThread.cs deleted file mode 100644 index fd2fa7b..0000000 --- a/src/MindControl/Threading/ParameterizedRemoteThread.cs +++ /dev/null @@ -1,36 +0,0 @@ -using MindControl.Native; - -namespace MindControl.Threading; - -/// -/// Represents an awaitable thread running an operation in another process, with parameters temporarily stored in the -/// target process memory. -/// -public class ParameterizedRemoteThread : RemoteThread -{ - private readonly MemoryReservation _parameterReservation; - - /// Initializes a new instance of the class. - /// Operating system service used to interact with the thread. - /// Handle to the thread. - /// Memory reservation containing the parameters to pass to the thread. The - /// reservation will automatically be freed when the thread is awaited to completion or when this instance is - /// disposed. - internal ParameterizedRemoteThread(IOperatingSystemService osService, IntPtr threadHandle, - MemoryReservation parameterReservation) - : base(osService, threadHandle) - { - _parameterReservation = parameterReservation; - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged - /// resources. - public override void Dispose() - { - if (IsDisposed) - return; - - base.Dispose(); - _parameterReservation.Dispose(); - } -} \ No newline at end of file diff --git a/test/MindControl.Test/AddressingTests/PointerPathTest.cs b/test/MindControl.Test/AddressingTests/PointerPathTest.cs index 65cd4db..e4b7786 100644 --- a/test/MindControl.Test/AddressingTests/PointerPathTest.cs +++ b/test/MindControl.Test/AddressingTests/PointerPathTest.cs @@ -184,6 +184,24 @@ public readonly struct ExpressionTestCase // Non valid expression cases new ExpressionTestCase + { + Expression = string.Empty, + ShouldBeValid = false, + Explanation = "An empty expression is invalid." + }, + new ExpressionTestCase + { + Expression = " ", + ShouldBeValid = false, + Explanation = "An all-whitespace expression is invalid." + }, + new ExpressionTestCase + { + Expression = "\"mymodulename.exe+1F016644,13,A0,0", + ShouldBeValid = false, + Explanation = "Double-quoted module names must be closed." + }, + new ExpressionTestCase { Expression = "mymoduleName.exe+FFFFFFFFFFFFFFFF+1-1,FFFFFFFFFFFFFFFF+1-1", ShouldBeValid = false, diff --git a/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs b/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs index 369d65d..846efa3 100644 --- a/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs +++ b/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs @@ -1,5 +1,4 @@ -using System.ComponentModel; -using MindControl.Results; +using MindControl.Results; using MindControl.Test.ProcessMemoryTests; using NUnit.Framework; @@ -38,7 +37,9 @@ public void DisposeTest() // Check that the allocation instance is now disposed and unusable Assert.That(_allocation.IsDisposed, Is.True); - Assert.Throws(() => _allocation.ReserveRange(0x10)); + var reserveAttemptResult = _allocation.ReserveRange(0x10); + Assert.That(reserveAttemptResult.IsSuccess, Is.False); + Assert.That(reserveAttemptResult.Error, Is.TypeOf()); // Check that the allocation has been removed from the list Assert.That(TestProcessMemory!.Allocations, Is.Empty); @@ -82,11 +83,15 @@ public void ReserveRangeWithFullRangeTest() /// /// Tests the method. - /// Attempt to reserve 0 bytes. This should throw. + /// Attempt to reserve 0 bytes. This should return a result. /// [Test] public void ReserveRangeWithZeroSizeTest() - => Assert.Throws(() => _allocation.ReserveRange(0)); + { + var reservationResult = _allocation.ReserveRange(0); + Assert.That(reservationResult.IsSuccess, Is.False); + Assert.That(reservationResult.Error, Is.TypeOf()); + } /// /// Tests the method. diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs index aec6ce0..eef68af 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs @@ -144,4 +144,30 @@ public void DisableCodeAtWithBadInstructionsTest() Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf()); } + + /// + /// Tests the method with a + /// detached process. Expects a . + /// + [Test] + public void DisableCodeWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory!.DisableCodeAt(FindMovLongAddress()); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + } + + /// + /// Tests the method with a + /// detached process. Expects a . + /// + [Test] + public void DisableCodeWithPointerPathWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory!.DisableCodeAt(FindMovLongAddress().ToString("X")); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs index a18a36b..7a40d9d 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs @@ -209,6 +209,66 @@ public void HookWithEmptyAssemblerTest() Assert.That(hookResult.IsFailure, Is.True); Assert.That(hookResult.Error, Is.TypeOf()); } + + /// + /// Tests with a + /// detached process. Expects a result. + /// + [Test] + public void HookWithDetachedProcessTest() + { + var assembler = new Assembler(64); + assembler.ret(); + TestProcessMemory!.Dispose(); + var hookResult = TestProcessMemory!.Hook(0x1234, assembler, + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests with a + /// detached process. Expects a result. + /// + [Test] + public void HookWithPointerPathWithDetachedProcessTest() + { + var assembler = new Assembler(64); + assembler.ret(); + TestProcessMemory!.Dispose(); + var hookResult = TestProcessMemory!.Hook("1234", assembler, + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests with a + /// detached process. Expects a result. + /// + [Test] + public void HookWithByteArrayWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var hookResult = TestProcessMemory!.Hook(0x1234, [0xCC], + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests with a + /// detached process. Expects a result. + /// + [Test] + public void HookWithByteArrayWithPointerPathWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var hookResult = TestProcessMemory!.Hook("1234", [0xCC], + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } /// /// Tests the method. @@ -250,8 +310,7 @@ public void InsertCodeAtWithByteArrayTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongNextInstructionAddress = - FindMovLongAddress() + 10; + var movLongNextInstructionAddress = FindMovLongAddress() + 10; // Insert the code right after our target MOV instruction. // That way, the RAX register will be set to the value we want before it's used to write the new long value. @@ -280,8 +339,7 @@ public void InsertCodeAtWithByteArrayWithIsolationTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongNextInstructionAddress = - FindMovLongAddress() + 10; + var movLongNextInstructionAddress = FindMovLongAddress() + 10; // Insert the code right after our target MOV instruction. // That way, the RAX register will be set to the value we want before it's used to write the new long value. @@ -305,8 +363,7 @@ public void InsertCodeAtWithAssemblerTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongNextInstructionAddress = - FindMovLongAddress() + 10; + var movLongNextInstructionAddress = FindMovLongAddress() + 10; var hookResult = TestProcessMemory!.InsertCodeAt(movLongNextInstructionAddress, assembler); Assert.That(hookResult.IsSuccess, Is.True); @@ -326,8 +383,7 @@ public void InsertCodeAtWithAssemblerWithIsolationTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongNextInstructionAddress = - FindMovLongAddress() + 10; + var movLongNextInstructionAddress = FindMovLongAddress() + 10; var hookResult = TestProcessMemory!.InsertCodeAt(movLongNextInstructionAddress, assembler, HookRegister.RaxEax); Assert.That(hookResult.IsSuccess, Is.True); @@ -347,8 +403,7 @@ public void InsertCodeAtWithByteArrayWithPointerPathTest() var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); var bytes = assembler.AssembleToBytes().Value; - var movLongNextInstructionAddress = - FindMovLongAddress() + 10; + var movLongNextInstructionAddress = FindMovLongAddress() + 10; var pointerPath = movLongNextInstructionAddress.ToString("X"); var hookResult = TestProcessMemory!.InsertCodeAt(pointerPath, bytes); @@ -370,8 +425,7 @@ public void InsertCodeAtWithAssemblerWithPointerPathTest() { var assembler = new Assembler(64); assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongNextInstructionAddress = - FindMovLongAddress() + 10; + var movLongNextInstructionAddress = FindMovLongAddress() + 10; var pointerPath = movLongNextInstructionAddress.ToString("X"); var hookResult = TestProcessMemory!.InsertCodeAt(pointerPath, assembler); diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index 80d2795..79df849 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -256,6 +256,19 @@ public void ReserveZeroTest() Assert.That(reserveResult.Error, Is.InstanceOf()); } + /// + /// Tests the method with a detached process. + /// Expects the result to be an . + /// + [Test] + public void ReserveWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var reserveResult = TestProcessMemory.Reserve(0x1000, false); + Assert.That(reserveResult.IsSuccess, Is.False); + Assert.That(reserveResult.Error, Is.InstanceOf()); + } + /// /// Tests the method. /// Stores a byte array in an allocated range and verifies that the value has been stored properly. @@ -354,6 +367,33 @@ public void StoreWithMultipleOverflowingValuesTest() Assert.That(secondStoreResult.IsSuccess, Is.True); Assert.That(TestProcessMemory!.Allocations, Has.Count.EqualTo(2)); } + + /// + /// Tests the method with a detached process. + /// Expects the result to be an . + /// + [Test] + public void StoreWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var storeResult = TestProcessMemory.Store(new byte[8]); + Assert.That(storeResult.IsSuccess, Is.False); + Assert.That(storeResult.Error, Is.InstanceOf()); + } + + /// + /// Tests the method with a detached process. + /// Expects the result to be an . + /// + [Test] + public void StoreWithAllocationWithDetachedProcessTest() + { + var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; + TestProcessMemory.Dispose(); + var storeResult = TestProcessMemory.Store(new byte[8], allocation); + Assert.That(storeResult.IsSuccess, Is.False); + Assert.That(storeResult.Error, Is.InstanceOf()); + } /// /// Tests the method. @@ -407,7 +447,7 @@ public void StoreStringWithAllocationTest() /// /// Tests the method. - /// Specify invalid settings. The result is expected to be an . + /// Specify invalid settings. The result is expected to be an . /// [Test] public void StoreStringWithoutAllocationWithInvalidSettingsTest() @@ -416,13 +456,13 @@ public void StoreStringWithoutAllocationWithInvalidSettingsTest() var result = TestProcessMemory!.StoreString("Hello world", invalidSettings); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error, Is.InstanceOf()); } /// /// Tests the method. /// Specify valid settings, but with a length prefix that is too short to store the provided string. The result is - /// expected to be an . + /// expected to be an . /// [Test] public void StoreStringWithoutAllocationWithIncompatibleSettingsTest() @@ -432,12 +472,12 @@ public void StoreStringWithoutAllocationWithIncompatibleSettingsTest() var result = TestProcessMemory!.StoreString(new string('a', 256), settingsThatCanOnlyStoreUpTo255Chars); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error, Is.InstanceOf()); } /// /// Tests the method. - /// Specify invalid settings. The result is expected to be an . + /// Specify invalid settings. The result is expected to be an . /// [Test] public void StoreStringWithAllocationWithInvalidSettingsTest() @@ -447,13 +487,13 @@ public void StoreStringWithAllocationWithInvalidSettingsTest() var result = TestProcessMemory!.StoreString("Hello world", invalidSettings, allocation); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error, Is.InstanceOf()); } /// /// Tests the method. /// Specify valid settings, but with a length prefix that is too short to store the provided string. The result is - /// expected to be an . + /// expected to be an . /// [Test] public void StoreStringWithAllocationWithIncompatibleSettingsTest() @@ -465,6 +505,33 @@ public void StoreStringWithAllocationWithIncompatibleSettingsTest() allocation); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// + /// Tests the method with a detached process. + /// Expects the result to be an . + /// + [Test] + public void StoreStringWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.StoreString("Hello world", GetDotNetStringSettings()); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// + /// Tests the method with a detached + /// process. Expects the result to be an . + /// + [Test] + public void StoreStringWithAllocationWithDetachedProcessTest() + { + var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; + TestProcessMemory.Dispose(); + var result = TestProcessMemory.StoreString("Hello world", GetDotNetStringSettings(), allocation); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs index ba45d8e..cd495ff 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs @@ -102,6 +102,19 @@ public void EvaluateWithUnknownModuleTest() var error = (PathEvaluationFailureOnBaseModuleNotFound)result.Error; Assert.That(error.ModuleName, Is.EqualTo("ThisModuleDoesNotExist.dll")); } + + /// + /// Tests with a detached process. + /// The operation is expected to fail with a . + /// + [Test] + public void EvaluateWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory!.EvaluateMemoryAddress(GetPathToMaxAddress()); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + } } /// diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs index 9a97de0..3d8be0e 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs @@ -35,7 +35,7 @@ public void SetUp() } /// - /// Tests the method. + /// Tests the method. /// After injecting the library, the target process should output "Injected library attached", which is the text /// printed by code run from the injected library. /// @@ -49,7 +49,7 @@ public void InjectLibraryTest() } /// - /// Tests the method. + /// Tests the method. /// Does the same as , but with a DLL that has a path containing spaces and non-ASCII /// characters. /// @@ -68,7 +68,7 @@ public void InjectLibraryWithNonAsciiPathTest() } /// - /// Tests the method. + /// Tests the method. /// Specify a path to a non-existent library file. /// The method should fail with a . /// @@ -83,6 +83,18 @@ public void InjectLibraryWithLibraryFileNotFoundTest() Assert.That(error.LibraryPath, Has.Length.GreaterThan(path.Length)); // We expect a full path Assert.That(error.LibraryPath, Does.EndWith("NonExistentLibrary.dll")); } + + /// + /// Tests the method. + /// + [Test] + public void InjectLibraryWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.InjectLibrary(GetInjectedLibraryPath()); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } } /// diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index 3ec3cfd..4bd4d1f 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -114,6 +114,32 @@ public void ReadBytesOnPartiallyUnreadableRangeTest() Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); } + /// + /// Tests with a detached process. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.ReadBytes(OuterClassPointer, 4); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + } + + /// + /// Tests with a detached process. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesWithPointerPathOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.ReadBytes(OuterClassPointer.ToString("X"), 4); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + } + #endregion #region ReadBytesPartial @@ -231,6 +257,32 @@ public void ReadBytesPartialOnPartiallyUnreadableRangeTest() Assert.That(result.Value, Is.EqualTo(4)); Assert.That(buffer, Is.EqualTo(new byte[] { 0x1, 0x2, 0x3, 0x4, 0, 0, 0, 0 })); } + + /// + /// Tests with a detached process. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesPartialOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.ReadBytesPartial(OuterClassPointer, new byte[4], 4); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + } + + /// + /// Tests with a detached process. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesPartialWithPointerPathOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.ReadBytesPartial(OuterClassPointer.ToString("X"), new byte[4], 4); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + } #endregion @@ -465,6 +517,106 @@ public void ReadIncompatibleTypeTest() Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnUnsupportedType))); } + /// + /// Tests the method with a pointer path that fails to evaluate. + /// It should fail with a . + /// + [Test] + public void ReadWithBadPointerPathTest() + { + var result = TestProcessMemory!.Read("bad pointer path"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); + } + + /// + /// Tests the method with a zero pointer. + /// It should fail with a . + /// + [Test] + public void ReadWithZeroPointerTest() + { + var result = TestProcessMemory!.Read(UIntPtr.Zero); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnZeroPointer))); + } + + /// + /// Tests the method with a detached process. + /// It should fail with a . + /// + [Test] + public void ReadOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.Read(OuterClassPointer); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + } + + /// + /// Tests the method with a detached process. + /// It should fail with a . + /// + [Test] + public void ReadWithPointerPathOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.Read(OuterClassPointer.ToString("X")); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + } + + /// + /// Tests the method with a pointer path that fails to evaluate. + /// It should fail with a . + /// + [Test] + public void ReadObjectWithBadPointerPathTest() + { + var result = TestProcessMemory!.Read(typeof(long), "bad pointer path"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); + } + + /// + /// Tests the method with a zero pointer. + /// It should fail with a . + /// + [Test] + public void ReadObjectWithZeroPointerTest() + { + var result = TestProcessMemory!.Read(typeof(long), UIntPtr.Zero); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnZeroPointer))); + } + + /// + /// Tests the method with a detached process. + /// It should fail with a . + /// + [Test] + public void ReadObjectOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.Read(typeof(long), OuterClassPointer); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + } + + /// + /// Tests the method with a detached process. + /// It should fail with a . + /// + [Test] + public void ReadObjectWithPointerPathOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.Read(typeof(long), OuterClassPointer.ToString("X")); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + } + #endregion #region Structure reading @@ -705,6 +857,32 @@ public void FindStringSettingsOnInvalidPointerPathTest() Assert.That(failure.Details, Is.Not.Null); } + /// + /// Tests with a detached process. + /// The method should return a . + /// + [Test] + public void FindStringSettingsOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.FindStringSettings(0x1234, InitialStringValue); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnDetachedProcess))); + } + + /// + /// Tests with a detached process. + /// The method should return a . + /// + [Test] + public void FindStringSettingsWithPointerPathOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.FindStringSettings("1234", InitialStringValue); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnDetachedProcess))); + } + #endregion #region ReadRawString @@ -824,6 +1002,32 @@ public void ReadRawStringWithBadPointerPathTest() var pathError = ((ReadFailureOnPointerPathEvaluation)result.Error).Details; Assert.That(pathError, Is.Not.Null); } + + /// + /// Tests with a detached + /// process. Expect the result to be a . + /// + [Test] + public void ReadRawStringWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.ReadRawString(0x1234, Encoding.Unicode); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + } + + /// + /// Tests with a detached + /// process. Expect the result to be a . + /// + [Test] + public void ReadRawStringWithPointerPathWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.ReadRawString("1234", Encoding.Unicode); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + } #endregion @@ -1090,6 +1294,32 @@ public void ReadStringPointerWithTooLongStringWithoutPrefixTest() var failure = (StringReadFailureOnStringTooLong)secondResult.Error; Assert.That(failure.LengthPrefixValue, Is.Null); // No prefix, so the value should be null. } + + /// + /// Tests with a detached process. + /// Expect the result to be a . + /// + [Test] + public void ReadStringPointerWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.ReadStringPointer(0x1234, GetDotNetStringSettings()); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnDetachedProcess))); + } + + /// + /// Tests with a detached process. + /// Expect the result to be a . + /// + [Test] + public void ReadStringPointerWithPointerPathWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.ReadStringPointer("1234", GetDotNetStringSettings()); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnDetachedProcess))); + } #endregion diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryThreadingTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryThreadingTest.cs index 3c2fae7..5eda774 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryThreadingTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryThreadingTest.cs @@ -2,6 +2,8 @@ using System.Runtime.InteropServices; using System.Text; using Iced.Intel; +using MindControl.Code; +using static Iced.Intel.AssemblerRegisters; using MindControl.Results; using MindControl.Threading; using NUnit.Framework; @@ -14,6 +16,38 @@ namespace MindControl.Test.ProcessMemoryTests; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ProcessMemoryThreadingTest : BaseProcessMemoryTest { + /// Holds arguments for the GetCurrentDirectoryW Win32 API function. + /// We use the attribute to control how the structure is arranged in memory. The Sequential layout prevents + /// the .net runtime from reordering fields, and the "Pack = 1" prevents padding in-between fields (otherwise, the + /// buffer address might start at byte 8 instead of the expected 4, for alignment/performance reasons). + [StructLayout(LayoutKind.Sequential, Pack = 1)] + protected struct GetCurrentDirectoryWArgs + { + /// Size of the destination buffer. + public uint BufferSize; + /// Address of the destination buffer. + public ulong BufferAddress; + } + + /// + /// Assembles instructions to call the GetCurrentDirectoryW kernel32.dll function. + /// + /// Address of the GetCurrentDirectoryW function. + /// An assembler object containing the assembled instructions. + protected virtual Assembler AssembleTrampolineForGetCurrentDirectoryW(UIntPtr functionAddress) + { + var assembler = new Assembler(64); + // In x64, the function uses the fastcall calling convention, i.e. RCX and RDX are used for the two arguments. + // When the thread is created, the thread parameter is in RCX. In this case, our parameter is going to be the + // address of a GetCurrentDirectoryWArgs struct holding the parameters we want. + assembler.mov(rax, rcx); // Move the address of the GetCurrentDirectoryWArgs struct to RAX, to free up RCX + assembler.mov(ecx, __dword_ptr[rax]); // Move the buffer size (first argument) to ECX/RCX + assembler.mov(rdx, __[rax+4]); // Move the buffer address (second argument) to RDX + assembler.call(functionAddress.ToUInt64()); // Call GetCurrentDirectoryW + assembler.ret(); + return assembler; + } + /// /// Tests . /// Runs the ExitProcess kernel32.dll function in a thread in the target process, and waits for the resulting @@ -31,37 +65,48 @@ public void RunThreadAndWaitTest() Assert.That(HasProcessExited, Is.True); // The process should have exited as a result of the function call } + /// + /// Tests . + /// Equivalent of , but with a pointer path. + /// + [Test] + public void RunThreadWithPointerPathTest() + { + var kernel32Module = TestProcessMemory!.GetModule("kernel32.dll"); + var functionAddress = kernel32Module!.ReadExportTable().Value["ExitProcess"]; + var threadResult = TestProcessMemory!.RunThread(functionAddress.ToString("X")); + Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Error.ToString()); + var waitResult = threadResult.Value.WaitForCompletion(TimeSpan.FromSeconds(10)); + Assert.That(waitResult.IsSuccess, Is.True, () => waitResult.Error.ToString()); + Assert.That(waitResult.Value, Is.Zero); + Assert.That(HasProcessExited, Is.True); + } + /// /// Tests . /// Writes a simple assembly code that calls the GetCurrentDirectoryW kernel32.dll function, runs that code in a - /// thread, and waits for the resulting thread to end. + /// thread with a struct holding the function arguments as a parameter, and waits for the resulting thread to end. /// We should be able to get the current directory of the target process. /// [Test] - public void RunThreadWithMultipleParametersTest() + public void RunThreadWithGetCurrentDirectoryWTrampolineTest() { var bufferReservation = TestProcessMemory!.Reserve(2048, false).Value; + var argsReservation = TestProcessMemory.Store(new GetCurrentDirectoryWArgs + { + BufferSize = 2048, + BufferAddress = bufferReservation.Address + }).Value; var kernel32Module = TestProcessMemory.GetModule("kernel32.dll"); var functionAddress = kernel32Module!.ReadExportTable().Value["GetCurrentDirectoryW"]; // We cannot call GetCurrentDirectoryW directly because of its parameters. We need to write a trampoline - // function that arranges the parameters in the right registers and then calls the target function. - var assembler = new Assembler(64); - var rcx = new AssemblerRegister64(Register.RCX); - var rdx = new AssemblerRegister64(Register.RDX); - // Use the fastcall calling convention, i.e. RCX and RDX are used for the first two arguments - assembler.mov(rcx, 2048); // Write the buffer size - assembler.mov(rdx, bufferReservation.Address.ToUInt64()); // Write the buffer address - assembler.call(functionAddress.ToUInt64()); // Call GetCurrentDirectoryW - assembler.ret(); - - // Write the code to the target process - var codeReservation = TestProcessMemory.Reserve(256, true, nearAddress: kernel32Module.GetRange().Start).Value; - var bytes = assembler.AssembleToBytes(codeReservation.Address).Value; - TestProcessMemory.Write(codeReservation.Address, bytes); + // function that prepares the parameters as they are expected by the function before calling it. + var assembler = AssembleTrampolineForGetCurrentDirectoryW(functionAddress); + var codeReservation = TestProcessMemory.StoreCode(assembler, kernel32Module.GetRange().Start).Value; // Run the thread - var threadResult = TestProcessMemory!.RunThread(codeReservation.Address); + var threadResult = TestProcessMemory!.RunThread(codeReservation.Address, argsReservation.Address); Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Error.ToString()); var waitResult = threadResult.Value.WaitForCompletion(TimeSpan.FromSeconds(10)); Assert.That(waitResult.IsSuccess, Is.True, () => waitResult.Error.ToString()); @@ -122,6 +167,96 @@ public async Task RunThreadSleepWaitAsyncTest() // Make sure to keep some leeway for the test to pass consistently, even in environments with scarce resources. Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(2000)); } + + /// + /// Tests with an invalid + /// pointer path. + /// Expects a error. + /// + [Test] + public void RunThreadWithInvalidPointerPathTest() + { + var threadResult = TestProcessMemory!.RunThread("invalid pointer path"); + Assert.That(threadResult.IsSuccess, Is.False); + Assert.That(threadResult.Error, Is.TypeOf()); + } + + /// + /// Tests with a zero address. + /// Expects a error. + /// + [Test] + public void RunThreadWithZeroPointerTest() + { + var threadResult = TestProcessMemory!.RunThread(UIntPtr.Zero); + Assert.That(threadResult.IsSuccess, Is.False); + Assert.That(threadResult.Error, Is.TypeOf()); + } + + /// + /// Tests with a module name + /// that does not match any module loaded in the process. + /// Expects a error. + /// + [Test] + public void RunThreadWithInvalidModuleTest() + { + var threadResult = TestProcessMemory!.RunThread("invalid module", "ExitProcess"); + Assert.That(threadResult.IsSuccess, Is.False); + Assert.That(threadResult.Error, Is.TypeOf()); + } + + /// + /// Tests with a valid module + /// name but a function name that does not match any exported function in the module. + /// Expects a error. + /// + [Test] + public void RunThreadWithInvalidFunctionTest() + { + var threadResult = TestProcessMemory!.RunThread("kernel32.dll", "invalid function"); + Assert.That(threadResult.IsSuccess, Is.False); + Assert.That(threadResult.Error, Is.TypeOf()); + } + + /// + /// Tests with a detached process. + /// Expects a error. + /// + [Test] + public void RunThreadWithAddressOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var threadResult = TestProcessMemory!.RunThread(0x1234); + Assert.That(threadResult.IsSuccess, Is.False); + Assert.That(threadResult.Error, Is.TypeOf()); + } + + /// + /// Tests with a detached + /// process. Expects a error. + /// + [Test] + public void RunThreadWithPointerPathOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var threadResult = TestProcessMemory!.RunThread("1234"); + Assert.That(threadResult.IsSuccess, Is.False); + Assert.That(threadResult.Error, Is.TypeOf()); + } + + /// + /// Tests with a detached + /// process. Expects a error. + /// + [Test] + public void RunThreadWithExportedFunctionOnDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var threadResult = TestProcessMemory!.RunThread("kernel32.dll", "Sleep", 2000); + Assert.That(threadResult.IsSuccess, Is.False); + Assert.That(threadResult.Error, Is.TypeOf()); + } } /// @@ -132,4 +267,54 @@ public class ProcessMemoryThreadingTestX86 : ProcessMemoryThreadingTest { /// Gets a boolean value defining which version of the target app is used. protected override bool Is64Bit => false; + + /// + /// Assembles instructions to call the GetCurrentDirectoryW kernel32.dll function. + /// + /// Address of the GetCurrentDirectoryW function. + /// An assembler object containing the assembled instructions. + protected override Assembler AssembleTrampolineForGetCurrentDirectoryW(UIntPtr functionAddress) + { + var assembler = new Assembler(32); + // In x86, this function uses the stdcall calling convention, i.e. the arguments are expected to be in the stack + // and pop in the right order (meaning they must be pushed in reverse order). + // When the thread is created, the thread parameter is in EBX. In this case, our parameter is going to be the + // address of a GetCurrentDirectoryWArgs struct holding the parameters of the function to call. + assembler.mov(eax, __[ebx + 4]); + assembler.push(eax); // Push the buffer address (second argument) + assembler.mov(eax, __[ebx]); + assembler.push(eax); // Push the buffer size (first argument) + assembler.call(functionAddress.ToUInt32()); // Call GetCurrentDirectoryW + assembler.add(esp, 8); // Clean up the stack (as per stdcall convention) + assembler.ret(); + return assembler; + } + + /// + /// Tests with an address that is + /// beyond the scope of a 32-bit process. + /// Expect a error. + /// + [Test] + public void RunThreadWithIncompatibleAddressTest() + { + UIntPtr maxAddress = uint.MaxValue; + var threadResult = TestProcessMemory!.RunThread(maxAddress + 1); + Assert.That(threadResult.IsSuccess, Is.False); + Assert.That(threadResult.Error, Is.TypeOf()); + } + + /// + /// Tests with a parameter + /// value that is beyond the scope of a 32-bit process. + /// Expect a error. + /// + [Test] + public void RunThreadWithIncompatibleParameterTest() + { + UIntPtr maxValue = uint.MaxValue; + var threadResult = TestProcessMemory!.RunThread("kernel32.dll", "ExitProcess", maxValue + 1); + Assert.That(threadResult.IsSuccess, Is.False); + Assert.That(threadResult.Error, Is.TypeOf()); + } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs index 643b1d0..852fece 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using MindControl.Results; +using NUnit.Framework; namespace MindControl.Test.ProcessMemoryTests; @@ -81,10 +82,95 @@ public void WriteStringPointerTest() // Test that the program actually used (wrote to the console) the string that we hacked in AssertFinalResults(IndexOfOutputString, newString); } + + /// + /// Tests with a zero pointer. + /// Expect a error. + /// + [Test] + public void WriteAtZeroPointerTest() + { + var result = TestProcessMemory!.Write(UIntPtr.Zero, 8); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// + /// Tests with a null value. + /// Expect a error. + /// + [Test] + public void WriteNullValueTest() + { + var result = TestProcessMemory!.Write(OuterClassPointer, (int?)null); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// + /// Tests with an unsupported + /// type. Expect a error. + /// + [Test] + public void WriteUnsupportedTypeTest() + { + var result = TestProcessMemory!.Write(OuterClassPointer, new object()); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// Defines a structure that is expected to be incompatible with writing methods. + private struct IncompatibleStruct { public long A; public byte[] B; } // The byte[] makes it incompatible + + /// + /// Tests with an incompatible + /// struct. Expect a error. + /// + /// + /// This test has been disabled because it triggers a System.AccessViolationException. This exception type used to + /// be impossible to catch by default in .net, and will still crash the NUnit test runner. It may be re-enabled in + /// the future if a solution is found. + /// + public void WriteIncompatibleStructTest() + { + var result = TestProcessMemory!.Write(OuterClassPointer, new IncompatibleStruct()); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// + /// Tests with a detached + /// process. Expect a error. + /// + [Test] + public void WriteWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.Write(OuterClassPointer, 8); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// + /// Tests with a detached + /// process. Expect a error. + /// + [Test] + public void WriteAtPointerPathWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var result = TestProcessMemory.Write(OuterClassPointer.ToString("X"), 8); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } } /// /// Runs the tests from with a 32-bit version of the target app. /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] -public class ProcessMemoryWriteTestX86 : ProcessMemoryWriteTest { protected override bool Is64Bit => false; } \ No newline at end of file +public class ProcessMemoryWriteTestX86 : ProcessMemoryWriteTest +{ + /// Gets a boolean value defining which version of the target app is used. + protected override bool Is64Bit => false; +} \ No newline at end of file diff --git a/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs b/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs index ade1f59..f1fc084 100644 --- a/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs +++ b/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs @@ -15,28 +15,28 @@ public class ByteSearchPatternTest { // Nominal case: only full bytes new("1F 49 1A 03", new PatternExpectedResult( - new byte[] { 0x1F, 0x49, 0x1A, 0x03 }, - new byte[] { 0xFF, 0xFF, 0xFF, 0xFF })), + [0x1F, 0x49, 0x1A, 0x03], + [0xFF, 0xFF, 0xFF, 0xFF])), // Full bytes and full wildcards new("1F ?? 1A 03", new PatternExpectedResult( - new byte[] { 0x1F, 0x00, 0x1A, 0x03 }, - new byte[] { 0xFF, 0x00, 0xFF, 0xFF })), + [0x1F, 0x00, 0x1A, 0x03], + [0xFF, 0x00, 0xFF, 0xFF])), // Partial wildcard (left) new("1F ?9 1A 03", new PatternExpectedResult( - new byte[] { 0x1F, 0x09, 0x1A, 0x03 }, - new byte[] { 0xFF, 0x0F, 0xFF, 0xFF })), + [0x1F, 0x09, 0x1A, 0x03], + [0xFF, 0x0F, 0xFF, 0xFF])), // Partial wildcard (right) new("1F 4? 1A 03", new PatternExpectedResult( - new byte[] { 0x1F, 0x40, 0x1A, 0x03 }, - new byte[] { 0xFF, 0xF0, 0xFF, 0xFF })), + [0x1F, 0x40, 0x1A, 0x03], + [0xFF, 0xF0, 0xFF, 0xFF])), // Mixed (all cases and odd spaces) new("1F4? ?A??", new PatternExpectedResult( - new byte[] { 0x1F, 0x40, 0x0A, 0x00 }, - new byte[] { 0xFF, 0xF0, 0x0F, 0x00 })), + [0x1F, 0x40, 0x0A, 0x00], + [0xFF, 0xF0, 0x0F, 0x00])), // Error case: odd number of characters new("1F 49 1A 0", null), From 583d1d3f365f7cb60bc56e9cb50fdf29f9b487c4 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sun, 30 Jun 2024 22:24:12 +0200 Subject: [PATCH 33/66] x86 allocation tests --- .../ProcessMemoryAllocationTest.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index 79df849..35cb7a3 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -100,6 +100,19 @@ public void AllocateZeroTest() Assert.That(allocateResult.IsSuccess, Is.False); Assert.That(allocateResult.Error, Is.InstanceOf()); } + + /// + /// Tests the method with a detached process. + /// This should return an . + /// + [Test] + public void AllocateWithDetachedProcessTest() + { + TestProcessMemory!.Dispose(); + var allocateResult = TestProcessMemory.Allocate(0x1000, false); + Assert.That(allocateResult.IsSuccess, Is.False); + Assert.That(allocateResult.Error, Is.InstanceOf()); + } /// /// Tests the method. @@ -534,4 +547,13 @@ public void StoreStringWithAllocationWithDetachedProcessTest() Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.InstanceOf()); } +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +public class ProcessMemoryAllocationTestX86 : ProcessMemoryAllocationTest +{ + /// Gets a boolean value defining which version of the target app is used. + protected override bool Is64Bit => false; } \ No newline at end of file From 4a5d0142721a29d54f39ba128ca37d05f66c9a52 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Wed, 3 Jul 2024 16:49:10 +0200 Subject: [PATCH 34/66] DisposableResult + missing IDisposables --- .../Allocation/MemoryAllocation.cs | 2 +- .../Allocation/MemoryReservation.cs | 2 +- .../ProcessMemory/ProcessMemory.Allocation.cs | 17 ++--- .../ProcessMemory/ProcessMemory.Injection.cs | 4 +- .../ProcessMemory/ProcessMemory.Threading.cs | 7 +- src/MindControl/Results/Result.cs | 70 +++++++++++++++++-- .../ProcessMemoryAllocationTest.cs | 1 + 7 files changed, 80 insertions(+), 23 deletions(-) diff --git a/src/MindControl/Allocation/MemoryAllocation.cs b/src/MindControl/Allocation/MemoryAllocation.cs index 8dea42a..9b13684 100644 --- a/src/MindControl/Allocation/MemoryAllocation.cs +++ b/src/MindControl/Allocation/MemoryAllocation.cs @@ -6,7 +6,7 @@ namespace MindControl; /// Represents a range of memory that has been allocated in a process. /// Can be used to safely manage data or code storage using reservations. /// -public class MemoryAllocation +public class MemoryAllocation : IDisposable { /// /// Gets the memory range allocated. diff --git a/src/MindControl/Allocation/MemoryReservation.cs b/src/MindControl/Allocation/MemoryReservation.cs index aa19e76..ce1684e 100644 --- a/src/MindControl/Allocation/MemoryReservation.cs +++ b/src/MindControl/Allocation/MemoryReservation.cs @@ -6,7 +6,7 @@ /// a process. /// Disposing a reservation will free the memory range for other uses. /// -public class MemoryReservation +public class MemoryReservation : IDisposable { /// /// Gets the memory range of this reservation. diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index 70c35f2..d6f32a8 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -24,7 +24,7 @@ public partial class ProcessMemory /// Specify this parameter to limit the allocation to a specific range of memory. /// If specified, try to allocate as close as possible to this address. /// A result holding either the allocated memory range, or an allocation failure. - public Result Allocate(ulong size, bool forExecutableCode, + public DisposableResult Allocate(ulong size, bool forExecutableCode, MemoryRange? limitRange = null, UIntPtr? nearAddress = null) { if (!IsAttached) @@ -195,7 +195,7 @@ private Result FindAndAllocateFreeMemory(ulong s /// If specified, prioritize allocations by their proximity to this address. If no /// matching allocation is found, a new allocation as close as possible to this address will be attempted. /// A result holding either the resulting reservation, or an allocation failure. - public Result Reserve(ulong size, bool requireExecutable, + public DisposableResult Reserve(ulong size, bool requireExecutable, MemoryRange? limitRange = null, UIntPtr? nearAddress = null) { if (!IsAttached) @@ -249,7 +249,7 @@ public Result Reserve(ulong size, bool req /// Data to store. /// Set to true if the data is executable code. Defaults to false. /// A result holding either the reserved memory range, or an allocation failure. - public Result Store(byte[] data, bool isCode = false) + public DisposableResult Store(byte[] data, bool isCode = false) { if (!IsAttached) return new StoreFailureOnDetachedProcess(); @@ -281,7 +281,7 @@ public Result Store(byte[] data, bool isCode = /// Data to store. /// Allocated memory to store the data. /// A result holding either the reservation storing the data, or a reservation failure. - public Result Store(byte[] data, MemoryAllocation allocation) + public DisposableResult Store(byte[] data, MemoryAllocation allocation) { if (!IsAttached) return new StoreFailureOnDetachedProcess(); @@ -311,7 +311,7 @@ public Result Store(byte[] data, MemoryAllocati /// Type of the value or structure. /// A result holding either the reservation where the data has been written, or an allocation failure. /// - public Result Store(T value) + public DisposableResult Store(T value) => Store(value.ToBytes(), false); /// @@ -326,7 +326,8 @@ public Result Store(T value) /// Type of the value or structure. /// A result holding either the reservation where the data has been written, or an allocation failure. /// - public Result Store(T value, MemoryAllocation allocation) where T: struct + public DisposableResult Store(T value, MemoryAllocation allocation) + where T: struct => Store(value.ToBytes(), allocation); /// @@ -337,7 +338,7 @@ public Result Store(T value, MemoryAllocatio /// String settings to use to write the string. /// A result holding either the reservation where the string has been written, or an allocation failure. /// - public Result StoreString(string value, StringSettings settings) + public DisposableResult StoreString(string value, StringSettings settings) { if (!IsAttached) return new StoreFailureOnDetachedProcess(); @@ -362,7 +363,7 @@ public Result StoreString(string value, StringS /// Range of memory to store the string in. /// A result holding either the reservation where the string has been written, or an allocation failure. /// - public Result StoreString(string value, StringSettings settings, + public DisposableResult StoreString(string value, StringSettings settings, MemoryAllocation allocation) { if (!IsAttached) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs b/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs index c211a79..420edd0 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs @@ -45,7 +45,7 @@ public Result InjectLibrary(string libraryPath, TimeSpan waitT var reservation = reservationResult.Value; // Run LoadLibraryW from inside the target process to have it load the library itself, which is usually safer - var threadResult = RunThread("kernel32.dll", "LoadLibraryW", reservation.Address); + using var threadResult = RunThread("kernel32.dll", "LoadLibraryW", reservation.Address); if (threadResult.IsFailure) return new InjectionFailureOnThreadFailure(threadResult.Error); @@ -59,8 +59,6 @@ public Result InjectLibrary(string libraryPath, TimeSpan waitT var moduleHandle = waitResult.Value; if (moduleHandle == 0) return new InjectionFailureOnLoadLibraryFailure(); - - reservation.Dispose(); // Free the parameter reservation as we don't need it anymore return Result.Success; } diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Threading.cs b/src/MindControl/ProcessMemory/ProcessMemory.Threading.cs index a548d98..d752f1f 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Threading.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Threading.cs @@ -21,7 +21,7 @@ public partial class ProcessMemory /// the expected way before calling the function. See the documentation for more info. /// A result holding either the thread instance that you can use to wait for the thread to return, or a /// error. - public Result RunThread(UIntPtr functionAddress, UIntPtr? parameter = null) + public DisposableResult RunThread(UIntPtr functionAddress, UIntPtr? parameter = null) { if (!IsAttached) return new ThreadFailureOnDetachedProcess(); @@ -45,7 +45,8 @@ public Result RunThread(UIntPtr functionAddress, UI /// the expected way before calling the function. See the documentation for more info. /// A result holding either the thread instance that you can use to wait for the thread to return, or a /// error. - public Result RunThread(PointerPath functionPointerPath, UIntPtr? parameter = null) + public DisposableResult RunThread(PointerPath functionPointerPath, + UIntPtr? parameter = null) { if (!IsAttached) return new ThreadFailureOnDetachedProcess(); @@ -70,7 +71,7 @@ public Result RunThread(PointerPath functionPointer /// the expected way before calling the function. See the documentation for more info. /// A result holding either the thread instance that you can use to wait for the thread to return, or a /// error. - public Result RunThread(string moduleName, string functionName, + public DisposableResult RunThread(string moduleName, string functionName, UIntPtr? parameter = null) { if (!IsAttached) diff --git a/src/MindControl/Results/Result.cs b/src/MindControl/Results/Result.cs index 8871667..4fb9309 100644 --- a/src/MindControl/Results/Result.cs +++ b/src/MindControl/Results/Result.cs @@ -93,7 +93,7 @@ public override string ToString() /// /// Type of the result that can be returned in case of success. /// Type of the error that can be returned in case of failure. -public sealed class Result : Result +public class Result : Result { private readonly TResult? _value; @@ -109,13 +109,13 @@ public sealed class Result : Result /// Initializes a new successful instance. /// /// Result of the operation. - private Result(TResult value) { _value = value; } + protected Result(TResult value) { _value = value; } /// /// Initializes a new failed instance. /// /// Error that caused the operation to fail. - private Result(TError error) : base(error) { } + protected Result(TError error) : base(error) { } /// /// Creates a new successful instance. @@ -156,14 +156,70 @@ public static implicit operator Result(TResult result) => FromResult(result); /// - /// Implicitly converts a result value to a successful instance. + /// Implicitly converts an error to an unsuccessful instance. /// - /// Result value to convert. - public static implicit operator Result(TError result) - => Failure(result); + /// Error to convert. + public static implicit operator Result(TError error) + => Failure(error); /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => IsSuccess ? Value?.ToString() ?? SuccessString : Error?.ToString() ?? FailureString; } + +/// +/// Represents the result of an operation that can either succeed or fail, with a result value in case of success. This +/// variant is disposable and may hold a disposable result. When a successful instance is disposed, the result will be +/// too. +/// +/// Type of the result that can be returned in case of success. +/// Type of the error that can be returned in case of failure. +public class DisposableResult : Result, IDisposable where TResult : IDisposable +{ + /// + /// Initializes a new successful instance. + /// + /// Result of the operation. + protected DisposableResult(TResult value) : base(value) { } + + /// + /// Initializes a new failed instance. + /// + /// Error that caused the operation to fail. + protected DisposableResult(TError error) : base(error) { } + + /// + /// Implicitly converts a result value to a successful instance. + /// + /// Result value to convert. + public static implicit operator DisposableResult(TResult result) + => FromResult(result); + + /// + /// Implicitly converts an error to an unsuccessful instance. + /// + /// Error to convert. + public static implicit operator DisposableResult(TError error) + => Failure(error); + + /// + /// Creates a new successful instance. + /// + /// Result of the operation. + public new static DisposableResult FromResult(TResult result) + => new(result); + + /// + /// Creates a new failed instance. + /// + /// Error that caused the operation to fail. + public new static DisposableResult Failure(TError error) => new(error); + + /// Disposes the result value if the operation was successful. + public void Dispose() + { + if (IsSuccess) + Value.Dispose(); + } +} diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index 35cb7a3..18a0c87 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -552,6 +552,7 @@ public void StoreStringWithAllocationWithDetachedProcessTest() /// /// Runs the tests from with a 32-bit version of the target app. /// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ProcessMemoryAllocationTestX86 : ProcessMemoryAllocationTest { /// Gets a boolean value defining which version of the target app is used. From 192a68cda5a6f363ae89c4fb1db1233278c936e1 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Thu, 4 Jul 2024 13:49:20 +0200 Subject: [PATCH 35/66] More x86 tests --- .../ProcessMemoryStreamTest.cs | 18 ++++-- .../BaseProcessMemoryCodeExtensionTest.cs | 32 +++++++++- .../ProcessMemoryCodeExtensionsTest.cs | 62 +++++++++++-------- .../ProcessMemoryFindBytesTest.cs | 10 +++ 4 files changed, 89 insertions(+), 33 deletions(-) diff --git a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs index cc35660..b80c037 100644 --- a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs +++ b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs @@ -374,7 +374,7 @@ public void WriteWithOffsetTest() [Test] public void WriteWithImpossibleOffsetTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); Assert.That(() => stream.Write(new byte[8], 8, 1), Throws.InstanceOf()); } @@ -386,7 +386,7 @@ public void WriteWithImpossibleOffsetTest() [Test] public void WriteWithImpossibleCountTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); Assert.That(() => stream.Write(new byte[8], 0, 9), Throws.InstanceOf()); } @@ -398,7 +398,7 @@ public void WriteWithImpossibleCountTest() [Test] public void WriteWithImpossibleOffsetAndCountTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); Assert.That(() => stream.Write(new byte[8], 3, 6), Throws.InstanceOf()); } @@ -431,9 +431,19 @@ public void WriteOnTheEdgeOfValidMemoryTest() [Test] public void WriteOnUnreadableMemoryTest() { - using var stream = TestProcessMemory!.GetMemoryStream(UIntPtr.MaxValue); + var stream = TestProcessMemory!.GetMemoryStream(UIntPtr.MaxValue); Assert.That(() => stream.Write(new byte[8], 0, 8), Throws.InstanceOf()); } #endregion +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryStreamTestX86 : ProcessMemoryStreamTest +{ + /// Gets a boolean value defining which version of the target app is used. + protected override bool Is64Bit => false; } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs index 21be665..fab5ede 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs @@ -6,6 +6,25 @@ /// public abstract class BaseProcessMemoryCodeExtensionTest : BaseProcessMemoryTest { + /// + /// Finds and returns the address of the MOV instruction that loads the new int value in the target app into the + /// RAX register, before assigning it to the output int value. + /// + protected UIntPtr FindMovIntAddress() + { + // Only search executable memory for two reasons: + // - It is faster. + // - When built on .net 8, there are 2 instances of the code. Only the right one is in executable memory. + + string signature = Is64Bit ? "C7 41 38 13 11 0F 00 48 8B 4D F8" + : "C7 41 28 13 11 0F 00 8B 4D F8"; + return TestProcessMemory!.FindBytes(signature, + settings: new FindBytesSettings { SearchExecutable = true }).First(); + + // x64: MOV [RCX+38],000F1113 + // x86: MOV [ECX+28],000F1113 + } + /// /// Finds and returns the address of the MOV instruction that loads the new long value in the target app into the /// RAX register, before assigning it to the output long value. @@ -16,10 +35,19 @@ protected UIntPtr FindMovLongAddress() // - It is faster. // - When built on .net 8, there are 2 instances of the code. Only the right one is in executable memory. - return TestProcessMemory!.FindBytes("48 B8 DF 54 09 2B BA 3C FD FF", + string signature = Is64Bit ? "48 B8 DF 54 09 2B BA 3C FD FF" + : "C7 01 DF 54 09 2B 8B 4D C4 C7 41 04 BA 3C FD FF"; + return TestProcessMemory!.FindBytes(signature, settings: new FindBytesSettings { SearchExecutable = true }).First(); + // For x64: // > MOV RAX, 0xFFFD3CBA2B0954DF ; Loads the new long value into the RAX register. This is the one we get. // MOV [RCX+20], RAX ; Changes the long value in the class instance that is output at the end. - } + + // For x86: + // MOV ECX,[EBP-3C] ; Loads the address of the class instance field into ECX + // > MOV [ECX],2B0954DF ; Writes the first 4 bytes of the long value in the class instance field + // MOV ECX,[EBP-3C] ; Loads the address of the class instance field into ECX + // MOV [ECX+4],FFFD3CBA ; Writes the last 4 bytes of the long value in the class instance field + } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs index eef68af..609be9a 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs @@ -1,4 +1,5 @@ -using MindControl.Code; +using System.Globalization; +using MindControl.Code; using MindControl.Results; using NUnit.Framework; @@ -12,25 +13,25 @@ public class ProcessMemoryCodeExtensionsTest : BaseProcessMemoryCodeExtensionTes { /// /// Tests the method. - /// The method is called on a MOV instruction that changes the value of the long value in the target app after the + /// The method is called on a MOV instruction that changes the value of the int value in the target app after the /// first step. - /// After disabling the instruction, we let the program run to the end, and check that the output long value is the + /// After disabling the instruction, we let the program run to the end, and check that the output int value is the /// original one (it was not modified because we disabled the instruction). /// [Test] public void DisableCodeAtTest() { - var movLongAddress = FindMovLongAddress() + 10; - var result = TestProcessMemory!.DisableCodeAt(movLongAddress); + var movIntAddress = FindMovIntAddress(); + var result = TestProcessMemory!.DisableCodeAt(movIntAddress); Assert.That(result.IsSuccess, Is.True); - Assert.That(result.Value.Address, Is.EqualTo(movLongAddress)); + Assert.That(result.Value.Address, Is.EqualTo(movIntAddress)); Assert.That(result.Value.Length, Is.AtLeast(1)); // We don't care how long it is but we check that it is set. ProceedUntilProcessEnds(); - // Test that the output long at index 5 is the first value set when the program starts, not the one that was + // Test that the output long is the initial value set when the program starts, not the one that was // supposed to be set by the disabled instruction. - AssertFinalResults(5, "-65746876815103"); + AssertFinalResults(IndexOfOutputInt, InitialIntValue.ToString(CultureInfo.InvariantCulture)); } /// @@ -40,35 +41,34 @@ public void DisableCodeAtTest() [Test] public void DisableCodeAtWithPointerPathTest() { - var movLongAddress = FindMovLongAddress() + 10; - var pointerPath = movLongAddress.ToString("X"); + var pointerPath = FindMovIntAddress().ToString("X"); var result = TestProcessMemory!.DisableCodeAt(pointerPath); Assert.That(result.IsSuccess, Is.True); ProceedUntilProcessEnds(); - AssertFinalResults(5, "-65746876815103"); + AssertFinalResults(IndexOfOutputInt, InitialIntValue.ToString(CultureInfo.InvariantCulture)); } /// /// Tests the method. - /// The method is called on a series of MOV instruction that changes the value of the long and ulong values in the + /// The method is called on a series of MOV instruction that changes the value of the int and uint values in the /// target app after the first step. - /// After disabling the instructions, we let the program run to the end, and check that the output long and ulong + /// After disabling the instructions, we let the program run to the end, and check that the output int and uint /// values are the original ones (they were not modified because we disabled the instructions). /// [Test] public void DisableCodeAtWithMultipleInstructionsTest() { - var movLongAddress = FindMovLongAddress(); - var result = TestProcessMemory!.DisableCodeAt(movLongAddress, 5); + var result = TestProcessMemory!.DisableCodeAt(FindMovIntAddress(), 3); Assert.That(result.IsSuccess, Is.True); ProceedUntilProcessEnds(); - // Test that the output long at index 5 and ulong at index 6 are the first values set when the program starts, - // not the ones that were supposed to be set by the disabled instructions. - Assert.That(FinalResults[5], Is.EqualTo("-65746876815103")); - Assert.That(FinalResults[6], Is.EqualTo("76354111324644")); + // Test that the output int and uint are the first values set when the program starts, instead of the ones that + // were supposed to be set by the disabled instructions. + Assert.That(FinalResults[IndexOfOutputInt], Is.EqualTo(InitialIntValue.ToString(CultureInfo.InvariantCulture))); + Assert.That(FinalResults[IndexOfOutputUInt], + Is.EqualTo(InitialUIntValue.ToString(CultureInfo.InvariantCulture))); } /// @@ -82,12 +82,11 @@ public void DisableCodeAtWithMultipleInstructionsTest() [Test] public void DisableCodeAtRevertTest() { - var movLongAddress = FindMovLongAddress() + 10; - var result = TestProcessMemory!.DisableCodeAt(movLongAddress); + var result = TestProcessMemory!.DisableCodeAt(FindMovIntAddress()); result.Value.Revert(); ProceedUntilProcessEnds(); - AssertFinalResults(5, ExpectedFinalValues[5]); + AssertExpectedFinalResults(); } /// @@ -111,8 +110,7 @@ public void DisableCodeAtWithZeroAddressTest() [Test] public void DisableCodeAtWithInvalidInstructionCountTest() { - var movLongAddress = FindMovLongAddress() + 10; - var result = TestProcessMemory!.DisableCodeAt(movLongAddress, 0); + var result = TestProcessMemory!.DisableCodeAt(FindMovIntAddress(), 0); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf()); } @@ -139,7 +137,7 @@ public void DisableCodeAtWithBadPointerPathTest() public void DisableCodeAtWithBadInstructionsTest() { var address = TestProcessMemory!.Reserve(0x1000, true).Value.Address; - TestProcessMemory.WriteBytes(address, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }); + TestProcessMemory.WriteBytes(address, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }); // Write invalid code var result = TestProcessMemory.DisableCodeAt(address); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf()); @@ -153,7 +151,7 @@ public void DisableCodeAtWithBadInstructionsTest() public void DisableCodeWithDetachedProcessTest() { TestProcessMemory!.Dispose(); - var result = TestProcessMemory!.DisableCodeAt(FindMovLongAddress()); + var result = TestProcessMemory!.DisableCodeAt(FindMovIntAddress()); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf()); } @@ -166,8 +164,18 @@ public void DisableCodeWithDetachedProcessTest() public void DisableCodeWithPointerPathWithDetachedProcessTest() { TestProcessMemory!.Dispose(); - var result = TestProcessMemory!.DisableCodeAt(FindMovLongAddress().ToString("X")); + var result = TestProcessMemory!.DisableCodeAt(FindMovIntAddress().ToString("X")); Assert.That(result.IsSuccess, Is.False); Assert.That(result.Error, Is.TypeOf()); } +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryCodeExtensionsTestX86 : ProcessMemoryCodeExtensionsTest +{ + /// Gets a boolean value defining which version of the target app is used. + protected override bool Is64Bit => false; } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs index 2876736..b9b66d8 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryFindBytesTest.cs @@ -96,4 +96,14 @@ public async Task FindBytesAsyncWithKnownFixedBytesPatternTest() // Verify that the result is within the range of the main module Assert.That(range.Contains(results.Single()), Is.True); } +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryFindBytesTestX86 : ProcessMemoryFindBytesTest +{ + /// Gets a boolean value defining which version of the target app is used. + protected override bool Is64Bit => false; } \ No newline at end of file From 8f6f6f5a02efd028dab67e4341ddd12b6a9e5e2c Mon Sep 17 00:00:00 2001 From: Doublevil Date: Fri, 5 Jul 2024 13:38:24 +0200 Subject: [PATCH 36/66] x86 hook tests --- .../ProcessMemory/ProcessMemory.Addressing.cs | 2 +- .../ProcessMemoryStreamTest.cs | 22 +- .../BaseProcessMemoryCodeExtensionTest.cs | 26 - .../ProcessMemoryHookExtensionsTest.cs | 752 ++++++++---------- 4 files changed, 361 insertions(+), 441 deletions(-) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs index e6f9e7a..9fabc18 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs @@ -87,7 +87,7 @@ public Result EvaluateMemoryAddress(PointerPath /// /// Pointer path to the starting address of the stream. /// A result holding either the created process memory stream, or a path evaluation failure. - public Result GetMemoryStream(PointerPath pointerPath) + public DisposableResult GetMemoryStream(PointerPath pointerPath) { var addressResult = EvaluateMemoryAddress(pointerPath); if (addressResult.IsFailure) diff --git a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs index b80c037..c763062 100644 --- a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs +++ b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs @@ -66,7 +66,7 @@ public void CanWriteTest() [Test] public void LengthTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); Assert.That(() => stream.Length, Throws.InstanceOf()); } @@ -77,7 +77,7 @@ public void LengthTest() [Test] public void SetLengthTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); Assert.That(() => stream.SetLength(0), Throws.InstanceOf()); Assert.That(() => stream.SetLength(32), Throws.InstanceOf()); Assert.That(() => stream.SetLength(256), Throws.InstanceOf()); @@ -114,7 +114,7 @@ public void PositionTest() [Test] public void FlushTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); Assert.That(() => stream.Flush(), Throws.Nothing); } @@ -160,7 +160,7 @@ public void SeekFromCurrentTest() [Test] public void SeekFromEndTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); Assert.That(() => stream.Seek(8, SeekOrigin.End), Throws.InstanceOf()); } @@ -176,7 +176,7 @@ public void SeekFromEndTest() [Test] public void SimpleReadTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer + 0x28); + using var stream = TestProcessMemory!.GetMemoryStream(GetPointerPathForValueAtIndex(IndexOfOutputULong)).Value; var buffer = new byte[8]; int byteCount = stream.Read(buffer, 0, 8); ulong readValue = MemoryMarshal.Read(buffer); @@ -194,7 +194,7 @@ public void SimpleReadTest() [Test] public void MultipleReadTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer + 0x20); + using var stream = TestProcessMemory!.GetMemoryStream(GetPointerPathForValueAtIndex(IndexOfOutputLong)).Value; var buffer = new byte[8]; int firstByteCount = stream.Read(buffer, 0, 8); long firstValue = MemoryMarshal.Read(buffer); @@ -217,7 +217,7 @@ public void MultipleReadTest() [Test] public void ReadWithOffsetTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer + 0x28); + using var stream = TestProcessMemory!.GetMemoryStream(GetPointerPathForValueAtIndex(IndexOfOutputULong)).Value; var buffer = new byte[12]; int byteCount = stream.Read(buffer, 4, 8); // Use an offset of 4 ulong readValue = MemoryMarshal.Read(buffer.AsSpan(4)); // Read value from index 4 @@ -235,7 +235,7 @@ public void ReadWithOffsetTest() [Test] public void ReadWithImpossibleOffsetTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); Assert.That(() => stream.Read(new byte[8], 8, 1), Throws.InstanceOf()); } @@ -247,7 +247,7 @@ public void ReadWithImpossibleOffsetTest() [Test] public void ReadWithImpossibleCountTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); Assert.That(() => stream.Read(new byte[8], 0, 9), Throws.InstanceOf()); } @@ -259,7 +259,7 @@ public void ReadWithImpossibleCountTest() [Test] public void ReadWithImpossibleOffsetAndCountTest() { - using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); + var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); Assert.That(() => stream.Read(new byte[8], 3, 6), Throws.InstanceOf()); } @@ -419,7 +419,7 @@ public void WriteOnTheEdgeOfValidMemoryTest() Assert.That(writeResult.IsSuccess, Is.True); // Attempt to write 8 bytes from the target address, which is 4 bytes before the end of the isolated segment. - using var stream = TestProcessMemory.GetMemoryStream(targetAddress); + var stream = TestProcessMemory.GetMemoryStream(targetAddress); Assert.That(() => stream.Write(new byte[8], 0, 8), Throws.InstanceOf()); } diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs index fab5ede..1380bd7 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/BaseProcessMemoryCodeExtensionTest.cs @@ -24,30 +24,4 @@ protected UIntPtr FindMovIntAddress() // x64: MOV [RCX+38],000F1113 // x86: MOV [ECX+28],000F1113 } - - /// - /// Finds and returns the address of the MOV instruction that loads the new long value in the target app into the - /// RAX register, before assigning it to the output long value. - /// - protected UIntPtr FindMovLongAddress() - { - // Only search executable memory for two reasons: - // - It is faster. - // - When built on .net 8, there are 2 instances of the code. Only the right one is in executable memory. - - string signature = Is64Bit ? "48 B8 DF 54 09 2B BA 3C FD FF" - : "C7 01 DF 54 09 2B 8B 4D C4 C7 41 04 BA 3C FD FF"; - return TestProcessMemory!.FindBytes(signature, - settings: new FindBytesSettings { SearchExecutable = true }).First(); - - // For x64: - // > MOV RAX, 0xFFFD3CBA2B0954DF ; Loads the new long value into the RAX register. This is the one we get. - // MOV [RCX+20], RAX ; Changes the long value in the class instance that is output at the end. - - // For x86: - // MOV ECX,[EBP-3C] ; Loads the address of the class instance field into ECX - // > MOV [ECX],2B0954DF ; Writes the first 4 bytes of the long value in the class instance field - // MOV ECX,[EBP-3C] ; Loads the address of the class instance field into ECX - // MOV [ECX+4],FFFD3CBA ; Writes the last 4 bytes of the long value in the class instance field - } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs index 7a40d9d..aaee9f0 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs @@ -1,4 +1,6 @@ -using Iced.Intel; +using System.Globalization; +using Iced.Intel; +using static Iced.Intel.AssemblerRegisters; using MindControl.Code; using MindControl.Hooks; using MindControl.Results; @@ -12,123 +14,132 @@ namespace MindControl.Test.ProcessMemoryTests.CodeExtensions; [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ProcessMemoryHookExtensionsTest : BaseProcessMemoryCodeExtensionTest { + protected const int AlternativeOutputIntValue = 123456; + + /// Builds an assembler that moves the alternative output int value in the class instance field. + protected virtual Assembler AssembleAlternativeMovInt() + { + var assembler = new Assembler(64); + assembler.mov(__dword_ptr[rcx+0x38], AlternativeOutputIntValue); + return assembler; + } + + /// Builds an assembler that moves the given value in register RCX/ECX. + protected virtual Assembler AssembleRcxMov(uint value) + { + var assembler = new Assembler(64); + assembler.mov(rcx, value); + return assembler; + } + #region Hook /// /// Tests the method. - /// The hook replaces a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the - /// long value in the target app, with a new MOV instruction that assigns a different value. + /// The hook replaces the MOV instruction that assigns the output int value in the target app with an alternative + /// MOV instruction that assigns a different value. /// No registers are preserved. /// After hooking, we let the program run, and check that the output long value is the one written by the hook. /// [Test] public void HookAndReplaceMovWithByteArrayTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = FindMovLongAddress(); - - // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. - var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + var movIntAddress = FindMovIntAddress(); + + var hookResult = TestProcessMemory!.Hook(movIntAddress, bytes, new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsSuccess, Is.True); Assert.That(hookResult.Value.InjectedCodeReservation, Is.Not.Null); - Assert.That(hookResult.Value.Address, Is.EqualTo(movLongAddress)); + Assert.That(hookResult.Value.Address, Is.EqualTo(movIntAddress)); Assert.That(hookResult.Value.Length, Is.AtLeast(5)); ProceedUntilProcessEnds(); - // After execution, the long in the output at index 5 must reflect the new value written by the hook. - AssertFinalResults(5, "1234567890"); + AssertFinalResults(IndexOfOutputInt, AlternativeOutputIntValue.ToString()); } /// /// Tests the method. - /// The hook targets a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the - /// long value in the target app, and inserts a new MOV instruction that assigns a different value after the - /// instruction. - /// However, we specify that the should be preserved. - /// After hooking, we let the program run, and check the output. The long value must be the expected, original one, - /// because we specified that the RAX register should be isolated. + /// The hook inserts a MOV instruction that changes RCX/ECX to the address of an empty section of memory before the + /// MOV instruction that assigns the output int value in the target app, which uses RCX/ECX. We specify that we do + /// not want to preserve any register. + /// After hooking, we let the program run, and check the output. The int value must be the initial one, because the + /// original instruction should write its new value in the empty section of memory specified by our injected code + /// instead. /// [Test] - public void HookAndInsertMovWithRegisterIsolationWithByteArrayTest() + public void HookAndInsertMovWithoutRegisterIsolationTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; + var reservation = TestProcessMemory!.Reserve(0x1000, false, MemoryRange.Full32BitRange).Value; + var bytes = AssembleRcxMov(reservation.Address.ToUInt32()).AssembleToBytes().Value; - var movLongAddress = FindMovLongAddress(); - - // Hook the instruction that writes the long value to RAX, to append code that writes another value. - // Specify that the RAX register should be isolated. - var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, - new HookOptions(HookExecutionMode.ExecuteOriginalInstructionFirst, HookRegister.RaxEax)); + var hookResult = TestProcessMemory!.Hook(FindMovIntAddress(), bytes, + new HookOptions(HookExecutionMode.ExecuteInjectedCodeFirst)); Assert.That(hookResult.IsSuccess, Is.True); ProceedUntilProcessEnds(); - // After execution, all values should be the expected ones. + Assert.That(FinalResults[IndexOfOutputInt], Is.EqualTo(InitialIntValue.ToString())); + } + + /// + /// Tests the method. + /// The hook inserts a MOV instruction that changes RCX/ECX to 0 before the MOV instruction that assigns the output + /// int value in the target app, which uses RCX/ECX. We specify that we want to preserve the RCX/ECX register. + /// After hooking, we let the program run, and check the output. The output int value must be the expected one, + /// because the RCX/ECX register is isolated (reverted to its original state after the injected code). + /// + [Test] + public void HookAndInsertMovWithRegisterIsolationWithByteArrayTest() + { + var bytes = AssembleRcxMov(0).AssembleToBytes().Value; + + var hookResult = TestProcessMemory!.Hook(FindMovIntAddress(), bytes, + new HookOptions(HookExecutionMode.ExecuteInjectedCodeFirst, HookRegister.RcxEcx)); + Assert.That(hookResult.IsSuccess, Is.True); + + ProceedUntilProcessEnds(); AssertExpectedFinalResults(); } /// /// Tests the method. - /// The hook replaces a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the - /// long value in the target app, with a new MOV instruction that assigns a different value. - /// No registers are preserved. - /// After hooking, we let the program run, and check that the output long value is the one written by the hook. + /// Equivalent of , but using an assembler to build the hook code. /// [Test] public void HookAndReplaceMovWithAssemblerTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = FindMovLongAddress(); - - // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. - var hookResult = TestProcessMemory!.Hook(movLongAddress, assembler, + var assembler = AssembleAlternativeMovInt(); + var targetInstructionAddress = FindMovIntAddress(); + var hookResult = TestProcessMemory!.Hook(targetInstructionAddress, assembler, new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsSuccess, Is.True); Assert.That(hookResult.Value.InjectedCodeReservation, Is.Not.Null); - Assert.That(hookResult.Value.Address, Is.EqualTo(movLongAddress)); + Assert.That(hookResult.Value.Address, Is.EqualTo(targetInstructionAddress)); Assert.That(hookResult.Value.Length, Is.AtLeast(5)); ProceedUntilProcessEnds(); - - // After execution, the long in the output at index 5 must reflect the new value written by the hook. - AssertFinalResults(5, "1234567890"); + AssertFinalResults(IndexOfOutputInt, AlternativeOutputIntValue.ToString()); } /// /// Tests the method. - /// The hook targets a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the - /// long value in the target app, and inserts a new MOV instruction that assigns a different value after the - /// instruction. - /// However, we specify that the should be preserved. - /// After hooking, we let the program run, and check the output. The long value must be the expected, original one, - /// because we specified that the RAX register should be isolated. + /// Equivalent of , but using an assembler to + /// build the hook code. /// [Test] public void HookAndInsertMovWithRegisterIsolationWithAssemblerTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); + var assembler = AssembleRcxMov(0); - var movLongAddress = FindMovLongAddress(); - - // Hook the instruction that writes the long value to RAX, to append code that writes another value. - // Specify that the RAX register should be isolated. - var hookResult = TestProcessMemory!.Hook(movLongAddress, assembler, - new HookOptions(HookExecutionMode.ExecuteOriginalInstructionFirst, HookRegister.RaxEax)); + var hookResult = TestProcessMemory!.Hook(FindMovIntAddress(), assembler, + new HookOptions(HookExecutionMode.ExecuteInjectedCodeFirst, HookRegister.RcxEcx)); Assert.That(hookResult.IsSuccess, Is.True); ProceedUntilProcessEnds(); - - // After execution, all values should be the expected ones. AssertExpectedFinalResults(); } @@ -170,9 +181,7 @@ public void HookWithBadPathWithByteArrayTest() [Test] public void HookWithBadPathWithAssemblerTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var hookResult = TestProcessMemory!.Hook(new PointerPath("bad pointer path"), assembler, + var hookResult = TestProcessMemory!.Hook(new PointerPath("bad pointer path"), AssembleAlternativeMovInt(), new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsFailure, Is.True); Assert.That(hookResult.Error, Is.TypeOf()); @@ -187,8 +196,7 @@ public void HookWithBadPathWithAssemblerTest() [Test] public void HookWithEmptyCodeArrayTest() { - var address = FindMovLongAddress(); - var hookResult = TestProcessMemory!.Hook(address, [], + var hookResult = TestProcessMemory!.Hook(FindMovIntAddress(), [], new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsFailure, Is.True); Assert.That(hookResult.Error, Is.TypeOf()); @@ -202,9 +210,8 @@ public void HookWithEmptyCodeArrayTest() [Test] public void HookWithEmptyAssemblerTest() { - var address = FindMovLongAddress(); - var assembler = new Assembler(64); - var hookResult = TestProcessMemory!.Hook(address, assembler, + var assembler = new Assembler(Is64Bit ? 64 : 32); + var hookResult = TestProcessMemory!.Hook(FindMovIntAddress(), assembler, new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsFailure, Is.True); Assert.That(hookResult.Error, Is.TypeOf()); @@ -217,7 +224,7 @@ public void HookWithEmptyAssemblerTest() [Test] public void HookWithDetachedProcessTest() { - var assembler = new Assembler(64); + var assembler = new Assembler(Is64Bit ? 64 : 32); assembler.ret(); TestProcessMemory!.Dispose(); var hookResult = TestProcessMemory!.Hook(0x1234, assembler, @@ -233,7 +240,7 @@ public void HookWithDetachedProcessTest() [Test] public void HookWithPointerPathWithDetachedProcessTest() { - var assembler = new Assembler(64); + var assembler = new Assembler(Is64Bit ? 64 : 32); assembler.ret(); TestProcessMemory!.Dispose(); var hookResult = TestProcessMemory!.Hook("1234", assembler, @@ -272,26 +279,21 @@ public void HookWithByteArrayWithPointerPathWithDetachedProcessTest() /// /// Tests the method. - /// The hook replaces a 10-bytes MOV instruction that feeds the RAX register with a new value to be assigned to the - /// long value in the target app, with a new MOV instruction that assigns a different value. - /// After hooking, we revert the hook. - /// We then let the program run, and check that the output long value is the original, expected one. + /// The hook is equivalent to the one built in , but we revert it + /// right after building it. + /// We then let the program run, and check that the output is the normal, expected one. /// [Test] public void HookRevertTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = FindMovLongAddress(); - - // Hook the instruction that writes the long value to RAX, and replace it with code that writes another value. - var hookResult = TestProcessMemory!.Hook(movLongAddress, bytes, + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + + var hookResult = TestProcessMemory!.Hook(FindMovIntAddress(), bytes, new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); hookResult.Value.Revert(); // Revert the hook to restore the original code. ProceedUntilProcessEnds(); - AssertFinalResults(5, ExpectedFinalValues[5]); + AssertExpectedFinalResults(); } #endregion @@ -299,522 +301,466 @@ public void HookRevertTest() #region InsertCodeAt /// - /// Tests the - /// method. - /// Inserts a new MOV instruction that assigns a different value after the instruction that writes a long value to - /// the RAX register. This should change the output long value. + /// Tests . + /// Inserts a MOV instruction that changes RCX/ECX before the MOV instruction that assigns the output int value in + /// the target app. The output int value must be the initial one. /// [Test] public void InsertCodeAtWithByteArrayTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; - var movLongNextInstructionAddress = FindMovLongAddress() + 10; + var reservation = TestProcessMemory!.Reserve(0x1000, false, MemoryRange.Full32BitRange).Value; + var bytes = AssembleRcxMov(reservation.Address.ToUInt32()).AssembleToBytes().Value; + var targetInstructionAddress = FindMovIntAddress(); - // Insert the code right after our target MOV instruction. - // That way, the RAX register will be set to the value we want before it's used to write the new long value. - var hookResult = TestProcessMemory!.InsertCodeAt(movLongNextInstructionAddress, bytes); + var hookResult = TestProcessMemory!.InsertCodeAt(targetInstructionAddress, bytes); Assert.That(hookResult.IsSuccess, Is.True); Assert.That(hookResult.Value.InjectedCodeReservation, Is.Not.Null); - Assert.That(hookResult.Value.Address, Is.EqualTo(movLongNextInstructionAddress)); + Assert.That(hookResult.Value.Address, Is.EqualTo(targetInstructionAddress)); Assert.That(hookResult.Value.Length, Is.AtLeast(5)); ProceedUntilProcessEnds(); - - // After execution, the long in the output at index 5 must reflect the new value written by the hook. - AssertFinalResults(5, "1234567890"); + AssertFinalResults(IndexOfOutputInt, InitialIntValue.ToString()); } /// - /// Tests the - /// method. - /// Inserts a new MOV instruction that assigns a different value after the instruction that writes a long value to - /// the RAX register. However, we specify that RAX should be preserved. - /// The output long value must be the original one, because the RAX register is isolated. + /// Tests . + /// Inserts a MOV instruction that changes RCX/ECX before the MOV instruction that assigns the output int value in + /// the target app, with RCX/ECX preservation. The output should be the normal, expected one (injected code should + /// not affect the original instructions because the register is preserved). /// [Test] public void InsertCodeAtWithByteArrayWithIsolationTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; - var movLongNextInstructionAddress = FindMovLongAddress() + 10; - - // Insert the code right after our target MOV instruction. - // That way, the RAX register will be set to the value we want before it's used to write the new long value. - // But because we specify that the RAX register should be preserved, after the hook, the RAX register will - // be restored to its original value. - var hookResult = TestProcessMemory!.InsertCodeAt(movLongNextInstructionAddress, bytes, HookRegister.RaxEax); + var bytes = AssembleRcxMov(0).AssembleToBytes().Value; + var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress(), bytes, HookRegister.RcxEcx); Assert.That(hookResult.IsSuccess, Is.True); + ProceedUntilProcessEnds(); AssertExpectedFinalResults(); } /// - /// Tests the - /// method. - /// Inserts a new MOV instruction that assigns a different value after the instruction that writes a long value to - /// the RAX register. This should change the output long value. + /// Tests . + /// Equivalent of , but using an assembler instead of a byte array. /// [Test] public void InsertCodeAtWithAssemblerTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongNextInstructionAddress = FindMovLongAddress() + 10; - var hookResult = TestProcessMemory!.InsertCodeAt(movLongNextInstructionAddress, assembler); + var reservation = TestProcessMemory!.Reserve(0x1000, false, MemoryRange.Full32BitRange).Value; + var assembler = AssembleRcxMov(reservation.Address.ToUInt32()); + var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress(), assembler); Assert.That(hookResult.IsSuccess, Is.True); + ProceedUntilProcessEnds(); - AssertFinalResults(5, "1234567890"); + AssertFinalResults(IndexOfOutputInt, InitialIntValue.ToString()); } /// - /// Tests the - /// method. - /// Inserts a new MOV instruction that assigns a different value after the instruction that writes a long value to - /// the RAX register. However, we specify that RAX should be preserved. - /// The output long value must be the original one, because the RAX register is isolated. + /// Tests . + /// Equivalent of , but using an assembler. /// [Test] public void InsertCodeAtWithAssemblerWithIsolationTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongNextInstructionAddress = FindMovLongAddress() + 10; - var hookResult = TestProcessMemory!.InsertCodeAt(movLongNextInstructionAddress, assembler, HookRegister.RaxEax); - + var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress(), AssembleRcxMov(0), HookRegister.RcxEcx); Assert.That(hookResult.IsSuccess, Is.True); ProceedUntilProcessEnds(); AssertExpectedFinalResults(); } /// - /// Tests the - /// method. - /// Inserts a new MOV instruction that assigns a different value after the instruction that writes a long value to - /// the RAX register. This should change the output long value. + /// Tests . + /// Equivalent of , but using a pointer path instead of an address. /// [Test] public void InsertCodeAtWithByteArrayWithPointerPathTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; - var movLongNextInstructionAddress = FindMovLongAddress() + 10; - var pointerPath = movLongNextInstructionAddress.ToString("X"); + var reservation = TestProcessMemory!.Reserve(0x1000, false, MemoryRange.Full32BitRange).Value; + var bytes = AssembleRcxMov(reservation.Address.ToUInt32()).AssembleToBytes().Value; + PointerPath targetInstructionPath = FindMovIntAddress().ToString("X"); - var hookResult = TestProcessMemory!.InsertCodeAt(pointerPath, bytes); + var hookResult = TestProcessMemory!.InsertCodeAt(targetInstructionPath, bytes); Assert.That(hookResult.IsSuccess, Is.True); ProceedUntilProcessEnds(); - AssertFinalResults(5, "1234567890"); + AssertFinalResults(IndexOfOutputInt, InitialIntValue.ToString()); } - /// - /// Tests the - /// - /// method. - /// Inserts a new MOV instruction that assigns a different value after the instruction that writes a long value to - /// the RAX register. This should change the output long value. + /// Tests + /// . + /// Equivalent of , but using a pointer path instead of an address. /// [Test] public void InsertCodeAtWithAssemblerWithPointerPathTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongNextInstructionAddress = FindMovLongAddress() + 10; - var pointerPath = movLongNextInstructionAddress.ToString("X"); + var reservation = TestProcessMemory!.Reserve(0x1000, false, MemoryRange.Full32BitRange).Value; + var assembler = AssembleRcxMov(reservation.Address.ToUInt32()); + var targetInstructionPath = FindMovIntAddress().ToString("X"); - var hookResult = TestProcessMemory!.InsertCodeAt(pointerPath, assembler); + var hookResult = TestProcessMemory!.InsertCodeAt(targetInstructionPath, assembler); Assert.That(hookResult.IsSuccess, Is.True); ProceedUntilProcessEnds(); - AssertFinalResults(5, "1234567890"); + AssertFinalResults(IndexOfOutputInt, InitialIntValue.ToString()); } - #endregion + /// Tests + /// . + /// Specifies a pointer path that does not evaluate to a valid address. + /// Expects a result. + /// + [Test] + public void InsertCodeAtWithByteArrayWithBadPointerTest() + { + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + var hookResult = TestProcessMemory!.InsertCodeAt("bad pointer path", bytes); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } - #region ReplaceCodeAt + /// Tests + /// . + /// Specifies a pointer path that does not evaluate to a valid address. + /// Expects a result. + /// + [Test] + public void InsertCodeAtWithAssemblerWithBadPointerTest() + { + var hookResult = TestProcessMemory!.InsertCodeAt("bad pointer path", AssembleAlternativeMovInt()); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests . + /// Specifies an address of 0. Expects a result. + /// + [Test] + public void InsertCodeAtWithByteArrayWithZeroPointerTest() + { + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + var hookResult = TestProcessMemory!.InsertCodeAt(0, bytes); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } /// - /// Tests the - /// - /// method. - /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV - /// instruction that assigns a different value. Only one instruction is replaced. - /// Expects the result to be a CodeChange (because the new code is expected to fit in place of the replaced code), - /// and the program output long value to be the one written by the new MOV instruction. + /// Tests . + /// Specifies an address of 0. Expects a result. /// [Test] - public void ReplaceCodeAtWithByteArrayTest() + public void InsertCodeAtWithAssemblerWithZeroPointerTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = FindMovLongAddress(); - - // Replace the code at the target MOV instruction. - var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, bytes); - Assert.That(hookResult.IsSuccess, Is.True); - Assert.That(hookResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); - - ProceedUntilProcessEnds(); - AssertFinalResults(5, "1234567890"); // The new value written by the injected code. + var hookResult = TestProcessMemory!.InsertCodeAt(0, AssembleAlternativeMovInt()); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); } /// - /// Tests the - /// - /// method. - /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV - /// instruction that assigns a different value. Two instructions are replaced. - /// Expects the result to be a CodeChange (because the new code is expected to fit in place of the replaced code), - /// and the program output long value to be the original initial value, because the next instruction that actually - /// changes the long value is replaced too, and the replacing code just moves a value to RAX, without actually - /// writing that value anywhere. + /// Tests . + /// Disposes the process memory instance and then call the method with valid parameters. + /// Expects a result. /// [Test] - public void ReplaceCodeAtWithByteArrayOnMultipleInstructionsTest() + public void InsertCodeAtWithByteArrayWithAddressWithDisposedInstanceTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = FindMovLongAddress(); - - // Replace the code at the target MOV instruction. - var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 2, bytes); - Assert.That(hookResult.IsSuccess, Is.True); - Assert.That(hookResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); - - ProceedUntilProcessEnds(); - AssertFinalResults(5, "-65746876815103"); // The initial value of the target long + TestProcessMemory!.Dispose(); + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress(), bytes); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); } /// - /// Tests the - /// - /// method. - /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV - /// instruction that assigns a different value, plus an instruction that sets RCX to zero. Only one instruction is - /// replaced. However, we specify that 4 registers should be preserved, including RCX, which is used by the next - /// original instruction to write the value at the right address. RAX is not preserved. - /// Expects the result to be a CodeHook (because of the register pop and push instructions, the new code should not - /// fit in place of the replaced code), and the program output long value to be the new value written to RAX. - /// Setting RCX to zero should not affect the output because that register should be preserved. + /// Tests . + /// Disposes the process memory instance and then call the method with valid parameters. + /// Expects a result. /// [Test] - public void ReplaceCodeAtWithByteArrayWithPreservedRegistersTest() + public void InsertCodeAtWithByteArrayWithPointerPathWithDisposedInstanceTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)123); - assembler.mov(new AssemblerRegister64(Register.RCX), (ulong)0); - var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = FindMovLongAddress(); - - // Replace the code at the target MOV instruction. - var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, bytes, - HookRegister.Flags, HookRegister.RcxEcx, HookRegister.RdxEdx, HookRegister.R8); - Assert.That(hookResult.IsSuccess, Is.True); - Assert.That(hookResult.Value, Is.TypeOf()); - - ProceedUntilProcessEnds(); - AssertFinalResults(5, "123"); + TestProcessMemory!.Dispose(); + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress().ToString("X"), bytes); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); } /// - /// Tests the - /// - /// method. - /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new sequence - /// of MOV instructions that assign a different value to the same register. Only one instruction is replaced, but - /// the replacing code is expected to be larger than the original instruction. - /// Expects the result to be a CodeHook (because the new code should not fit in place of the replaced code), and the - /// program output long value to be the one written by the new MOV instruction. + /// Tests . + /// Disposes the process memory instance and then call the method with valid parameters. + /// Expects a result. /// [Test] - public void ReplaceCodeAtWithByteArrayWithLargerCodeTest() + public void InsertCodeAtWithAssemblerWithAddressWithDisposedInstanceTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)9991234560); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)8881234560); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)7771234560); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = FindMovLongAddress(); - - // Replace the code at the target MOV instruction. - var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, bytes); - Assert.That(hookResult.IsSuccess, Is.True); - Assert.That(hookResult.Value, Is.TypeOf()); + TestProcessMemory!.Dispose(); + var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress(), AssembleAlternativeMovInt()); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// Tests + /// . + /// Disposes the process memory instance and then call the method with valid parameters. + /// Expects a result. + /// + [Test] + public void InsertCodeAtWithAssemblerWithPointerPathWithDisposedInstanceTest() + { + TestProcessMemory!.Dispose(); + var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress().ToString("X"), + AssembleAlternativeMovInt()); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + #endregion + + #region ReplaceCodeAt + + /// + /// Tests . + /// Replaces the MOV instruction that assigns the output int value in the target app with an alternative MOV + /// instruction that assigns a different value. After replacing, we let the program run, and check that the output + /// int value is the one written by the replacement code. + /// + [Test] + public void ReplaceCodeAtWithByteArrayTest() + { + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + + var replaceResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 1, bytes); + Assert.That(replaceResult.IsSuccess, Is.True); + // Check that the result is a CodeChange and not a hook, because the new instructions should fit. + Assert.That(replaceResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); ProceedUntilProcessEnds(); - AssertFinalResults(5, "1234567890"); // The new value written by the hook. + AssertFinalResults(IndexOfOutputInt, AlternativeOutputIntValue.ToString(CultureInfo.InvariantCulture)); } /// - /// Tests the - /// - /// method. - /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV - /// instruction that assigns a different value. Only one instruction is replaced. - /// Expects the output long value to be the one written by the new MOV instruction. + /// Tests . + /// Replaces the MOV instruction that assigns the output int value and the next 2 instructions in the target app + /// with an alternative MOV instruction that assigns a different value. After replacing, we let the program run, and + /// check that the output int value is the one written by the replacement code, but also that the output uint value + /// that should have been written right after the target instruction is the initial, untouched one. /// [Test] - public void ReplaceCodeAtWithByteArrayWithPointerPathTest() + public void ReplaceCodeAtWithByteArrayOnMultipleInstructionsTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = FindMovLongAddress(); - PointerPath pointerPath = movLongAddress.ToString("X"); + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; - // Replace the code at the target MOV instruction. - var hookResult = TestProcessMemory!.ReplaceCodeAt(pointerPath, 1, bytes); - Assert.That(hookResult.IsSuccess, Is.True); + var replaceResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 3, bytes); + Assert.That(replaceResult.IsSuccess, Is.True); + Assert.That(replaceResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); ProceedUntilProcessEnds(); - AssertFinalResults(5, "1234567890"); // The new value written by the hook. + Assert.That(FinalResults[IndexOfOutputInt], + Is.EqualTo(AlternativeOutputIntValue.ToString(CultureInfo.InvariantCulture))); + Assert.That(FinalResults[IndexOfOutputUInt], + Is.EqualTo(InitialUIntValue.ToString(CultureInfo.InvariantCulture))); } /// - /// Tests the - /// - /// method. - /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV - /// instruction that assigns a different value. Only one instruction is replaced. - /// Expects the result to be a CodeChange (because the new code is expected to fit in place of the replaced code), - /// and the program output long value to be the one written by the new MOV instruction. + /// Tests . + /// Replaces the MOV instruction that assigns the output int value in the target app with an alternative MOV + /// instruction that assigns a different value, and specify that we want to preserve register RAX. Because the + /// replaced instruction is the same as the replacement instruction, it would normally fit and not trigger a hook. + /// However, because we have register preservation code that adds a couple bytes to the replacement code, it cannot + /// fit anymore. This means a hook should be performed, and so we should get a CodeHook result. + /// After replacing, we let the program run, and check that the output int value is the one written by the + /// replacement code. /// [Test] - public void ReplaceCodeAtWithAssemblerTest() + public void ReplaceCodeAtWithByteArrayWithPreservedRegistersTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = FindMovLongAddress(); - - // Replace the code at the target MOV instruction. - var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, assembler); + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + var hookResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 1, bytes, HookRegister.RaxEax); Assert.That(hookResult.IsSuccess, Is.True); - Assert.That(hookResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); + Assert.That(hookResult.Value, Is.TypeOf()); ProceedUntilProcessEnds(); - AssertFinalResults(5, "1234567890"); // The new value written by the injected code. + AssertFinalResults(IndexOfOutputInt, AlternativeOutputIntValue.ToString(CultureInfo.InvariantCulture)); } - /// - /// Tests the - /// - /// method. - /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV - /// instruction that assigns a different value. Two instructions are replaced. - /// Expects the result to be a CodeChange (because the new code is expected to fit in place of the replaced code), - /// and the program output long value to be the original initial value, because the next instruction that actually - /// changes the long value is replaced too, and the replacing code just moves a value to RAX, without actually - /// writing that value anywhere. + /// Tests + /// . + /// Equivalent of , but using a pointer path instead of an address. /// [Test] - public void ReplaceCodeAtWithAssemblerOnMultipleInstructionsTest() + public void ReplaceCodeAtWithByteArrayWithPointerPathTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = FindMovLongAddress(); + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + PointerPath targetPath = FindMovIntAddress().ToString("X"); - // Replace the code at the target MOV instruction. - var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 2, assembler); - Assert.That(hookResult.IsSuccess, Is.True); - Assert.That(hookResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); + var replaceResult = TestProcessMemory!.ReplaceCodeAt(targetPath, 1, bytes); + Assert.That(replaceResult.IsSuccess, Is.True); + Assert.That(replaceResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); ProceedUntilProcessEnds(); - AssertFinalResults(5, "-65746876815103"); // The initial value of the target long + AssertFinalResults(IndexOfOutputInt, AlternativeOutputIntValue.ToString(CultureInfo.InvariantCulture)); } - /// - /// Tests the - /// - /// method. - /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV - /// instruction that assigns a different value, plus an instruction that sets RCX to zero. Only one instruction is - /// replaced. However, we specify that 4 registers should be preserved, including RCX, which is used by the next - /// original instruction to write the value at the right address. RAX is not preserved. - /// Expects the result to be a CodeHook (because of the register pop and push instructions, the new code should not - /// fit in place of the replaced code), and the program output long value to be the new value written to RAX. - /// Setting RCX to zero should not affect the output because that register should be preserved. + /// Tests + /// . + /// Equivalent of , but using an assembler instead of a byte array. /// [Test] - public void ReplaceCodeAtWithAssemblerWithPreservedRegistersTest() + public void ReplaceCodeAtWithAssemblerTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)123); - assembler.mov(new AssemblerRegister64(Register.RCX), (ulong)0); - var movLongAddress = FindMovLongAddress(); + var assembler = AssembleAlternativeMovInt(); - // Replace the code at the target MOV instruction. - var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, assembler, - HookRegister.Flags, HookRegister.RcxEcx, HookRegister.RdxEdx, HookRegister.R8); - Assert.That(hookResult.IsSuccess, Is.True); - Assert.That(hookResult.Value, Is.TypeOf()); + var replaceResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 1, assembler); + Assert.That(replaceResult.IsSuccess, Is.True); + Assert.That(replaceResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); ProceedUntilProcessEnds(); - AssertFinalResults(5, "123"); + AssertFinalResults(IndexOfOutputInt, AlternativeOutputIntValue.ToString(CultureInfo.InvariantCulture)); } - /// - /// Tests the - /// - /// method. - /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new sequence - /// of MOV instructions that assign a different value to the same register. Only one instruction is replaced, but - /// the replacing code is expected to be larger than the original instruction. - /// Expects the result to be a CodeHook (because the new code should not fit in place of the replaced code), and the - /// program output long value to be the one written by the new MOV instruction. + /// Tests + /// . + /// Equivalent of , but using an assembler instead + /// of a byte array. /// [Test] - public void ReplaceCodeAtWithAssemblerWithLargerCodeTest() + public void ReplaceCodeAtWithAssemblerWithPreservedRegistersTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)9991234560); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)8881234560); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)7771234560); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = FindMovLongAddress(); - - // Replace the code at the target MOV instruction. - var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, assembler); + var assembler = AssembleAlternativeMovInt(); + var hookResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 1, assembler, HookRegister.RaxEax); Assert.That(hookResult.IsSuccess, Is.True); Assert.That(hookResult.Value, Is.TypeOf()); ProceedUntilProcessEnds(); - AssertFinalResults(5, "1234567890"); // The new value written by the hook. + AssertFinalResults(IndexOfOutputInt, AlternativeOutputIntValue.ToString(CultureInfo.InvariantCulture)); } - /// - /// Tests the - /// - /// method. - /// Replace the code at the target MOV instruction that writes a long value to the RAX register with a new MOV - /// instruction that assigns a different value. Only one instruction is replaced. - /// Expects the output long value to be the one written by the new MOV instruction. + /// Tests + /// . + /// Equivalent of , but using a pointer path instead of an address. /// [Test] public void ReplaceCodeAtWithAssemblerWithPointerPathTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = FindMovLongAddress(); - PointerPath pointerPath = movLongAddress.ToString("X"); + var assembler = AssembleAlternativeMovInt(); + PointerPath targetPath = FindMovIntAddress().ToString("X"); - // Replace the code at the target MOV instruction. - var hookResult = TestProcessMemory!.ReplaceCodeAt(pointerPath, 1, assembler); - Assert.That(hookResult.IsSuccess, Is.True); + var replaceResult = TestProcessMemory!.ReplaceCodeAt(targetPath, 1, assembler); + Assert.That(replaceResult.IsSuccess, Is.True); + Assert.That(replaceResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); ProceedUntilProcessEnds(); - AssertFinalResults(5, "1234567890"); // The new value written by the hook. + AssertFinalResults(IndexOfOutputInt, AlternativeOutputIntValue.ToString(CultureInfo.InvariantCulture)); } - /// - /// Tests the - /// - /// method, calling it with a pointer path that does not evaluate to a valid address, but otherwise valid - /// parameters. + /// Tests + /// . + /// Use a pointer path that does not evaluate to a valid address, but otherwise valid parameters. /// Expects the result to be a . /// [Test] public void ReplaceCodeAtWithByteArrayWithBadPointerPathTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; - PointerPath pointerPath = "bad pointer path"; - var hookResult = TestProcessMemory!.ReplaceCodeAt(pointerPath, 1, bytes); + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + var hookResult = TestProcessMemory!.ReplaceCodeAt("bad pointer path", 1, bytes); Assert.That(hookResult.IsSuccess, Is.False); Assert.That(hookResult.Error, Is.TypeOf()); } - /// - /// Tests the - /// - /// method, calling it with a pointer path that does not evaluate to a valid address, but otherwise valid - /// parameters. - /// Expects the result to be a . + /// Tests + /// . + /// Equivalent of , but using an assembler instead of + /// a byte array. /// [Test] public void ReplaceCodeAtWithAssemblerWithBadPointerPathTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - PointerPath pointerPath = "bad pointer path"; - var hookResult = TestProcessMemory!.ReplaceCodeAt(pointerPath, 1, assembler); + var hookResult = TestProcessMemory!.ReplaceCodeAt("bad pointer path", 1, AssembleAlternativeMovInt()); Assert.That(hookResult.IsSuccess, Is.False); Assert.That(hookResult.Error, Is.TypeOf()); } - /// - /// Tests the - /// - /// method, calling it with an empty code array, but otherwise valid parameters. + /// Tests + /// . + /// Specify an empty code array, but otherwise valid parameters. /// Expects the result to be a . /// [Test] public void ReplaceCodeAtWithByteArrayWithNoCodeTest() { - var movLongAddress = FindMovLongAddress(); - var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, []); + var hookResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 1, []); Assert.That(hookResult.IsSuccess, Is.False); Assert.That(hookResult.Error, Is.TypeOf()); } - /// - /// Tests the - /// - /// method, calling it with an empty assembler, but otherwise valid parameters. + /// Tests + /// . + /// Specify an empty assembler, but otherwise valid parameters. /// Expects the result to be a . /// [Test] public void ReplaceCodeAtWithAssemblerWithNoCodeTest() { - var movLongAddress = FindMovLongAddress(); - var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 1, new Assembler(64)); + var hookResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 1, new Assembler(Is64Bit ? 64 : 32)); Assert.That(hookResult.IsSuccess, Is.False); Assert.That(hookResult.Error, Is.TypeOf()); } - /// - /// Tests the - /// - /// method, calling it with a number of instructions to replace of 0, but otherwise valid parameters. + /// Tests + /// . + /// Specifies a number of instructions to replace of 0, but otherwise valid parameters. /// Expects the result to be a . /// [Test] public void ReplaceCodeAtWithByteArrayWithZeroInstructionTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var bytes = assembler.AssembleToBytes().Value; - var movLongAddress = FindMovLongAddress(); - var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 0, bytes); + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + var hookResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 0, bytes); Assert.That(hookResult.IsSuccess, Is.False); Assert.That(hookResult.Error, Is.TypeOf()); } - /// - /// Tests the - /// - /// method, calling it with a number of instructions to replace of 0, but otherwise valid parameters. - /// Expects the result to be a . + /// Tests + /// . + /// Equivalent of , but using an assembler instead of + /// a byte array. /// [Test] public void ReplaceCodeAtWithAssemblerWithZeroInstructionTest() { - var assembler = new Assembler(64); - assembler.mov(new AssemblerRegister64(Register.RAX), (ulong)1234567890); - var movLongAddress = FindMovLongAddress(); - var hookResult = TestProcessMemory!.ReplaceCodeAt(movLongAddress, 0, assembler); + var hookResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 0, AssembleAlternativeMovInt()); Assert.That(hookResult.IsSuccess, Is.False); Assert.That(hookResult.Error, Is.TypeOf()); } #endregion +} + +/// +/// Runs the tests with a 32-bit target process. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryHookExtensionsTestX86 : ProcessMemoryHookExtensionsTest +{ + /// Gets a boolean value defining which version of the target app is used. + protected override bool Is64Bit => false; + + /// Builds an assembler that moves the alternative output int value in the class instance field. + protected override Assembler AssembleAlternativeMovInt() + { + var assembler = new Assembler(32); + assembler.mov(__dword_ptr[ecx+0x28], AlternativeOutputIntValue); + return assembler; + } + + /// Builds an assembler that moves the given value in register RCX/ECX. + protected override Assembler AssembleRcxMov(uint value) + { + var assembler = new Assembler(32); + assembler.mov(ecx, value); + return assembler; + } } \ No newline at end of file From 86ab4ba512cd2329e9b2e209c1e4ba32a9812ac9 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Fri, 5 Jul 2024 20:52:10 +0200 Subject: [PATCH 37/66] Fixed a test adapter test duplicate issue --- .../AddressingTests/PointerExtensionsTest.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/MindControl.Test/AddressingTests/PointerExtensionsTest.cs b/test/MindControl.Test/AddressingTests/PointerExtensionsTest.cs index 90e03a8..bfcefbd 100644 --- a/test/MindControl.Test/AddressingTests/PointerExtensionsTest.cs +++ b/test/MindControl.Test/AddressingTests/PointerExtensionsTest.cs @@ -13,8 +13,10 @@ public class PointerExtensionsTest [TestCase((ulong)0x1000, (ulong)0x2000, ExpectedResult = (ulong)0x1000)] [TestCase((ulong)0x2000, (ulong)0x1000, ExpectedResult = (ulong)0x1000)] [TestCase((ulong)0x1000, (ulong)0x1000, ExpectedResult = (ulong)0)] - [TestCase((ulong)0, ulong.MaxValue, ExpectedResult = ulong.MaxValue)] - [TestCase(ulong.MaxValue, (ulong)0, ExpectedResult = ulong.MaxValue)] + [TestCase((ulong)0, ulong.MaxValue, ExpectedResult = ulong.MaxValue, + TestName = "DistanceToTest(0,ulong.MaxValue)")] // Specifying the name fixes a test duplication bug + [TestCase(ulong.MaxValue, (ulong)0, ExpectedResult = ulong.MaxValue, + TestName = "DistanceToTest(ulong.MaxValue,0)")] // Specifying the name fixes a test duplication bug public ulong DistanceToTest(ulong value1, ulong value2) { var ptr1 = new UIntPtr(value1); From f588d4f904cece0b23d0d9db0f4faac1c0e1b62c Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 6 Jul 2024 00:12:31 +0200 Subject: [PATCH 38/66] Evaluate & read x86-specific tests --- .../ProcessMemory/ProcessMemory.Addressing.cs | 2 + .../ProcessMemory/ProcessMemory.Read.cs | 8 ++ .../Results/FindStringSettingsFailure.cs | 17 ++++ .../ProcessMemoryEvaluateTest.cs | 26 ++++++ .../ProcessMemoryReadTest.cs | 91 +++++++++++++++++++ 5 files changed, 144 insertions(+) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs index 9fabc18..d2d5fce 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs @@ -49,6 +49,8 @@ public Result EvaluateMemoryAddress(PointerPath // Check if the base address is valid if (baseAddress == UIntPtr.Zero) return new PathEvaluationFailureOnPointerOutOfRange(UIntPtr.Zero, PointerOffset.Zero); + if (!IsBitnessCompatible(baseAddress.Value)) + return new PathEvaluationFailureOnIncompatibleBitness(baseAddress.Value); // Follow the pointer path offset by offset var currentAddress = baseAddress.Value; diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs index e225c52..9eeb5b3 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs @@ -352,6 +352,10 @@ public Result FindStringSettings(UInt { if (!IsAttached) return new FindStringSettingsFailureOnDetachedProcess(); + if (stringPointerAddress == UIntPtr.Zero) + return new FindStringSettingsFailureOnZeroPointer(); + if (!IsBitnessCompatible(stringPointerAddress)) + return new FindStringSettingsFailureOnIncompatibleBitness(stringPointerAddress); if (string.IsNullOrWhiteSpace(expectedString)) return new FindStringSettingsFailureOnNoSettingsFound(); @@ -523,6 +527,10 @@ public Result ReadRawString(UIntPtr address, Encoding encod { if (!IsAttached) return new ReadFailureOnDetachedProcess(); + if (address == UIntPtr.Zero) + return new ReadFailureOnZeroPointer(); + if (!IsBitnessCompatible(address)) + return new ReadFailureOnIncompatibleBitness(address); if (maxLength is < 0) return new ReadFailureOnInvalidArguments("The maximum length cannot be negative."); if (maxLength == 0) diff --git a/src/MindControl/Results/FindStringSettingsFailure.cs b/src/MindControl/Results/FindStringSettingsFailure.cs index ed70df2..6929f3e 100644 --- a/src/MindControl/Results/FindStringSettingsFailure.cs +++ b/src/MindControl/Results/FindStringSettingsFailure.cs @@ -7,6 +7,9 @@ public enum FindStringSettingsFailureReason DetachedProcess, /// Failure when trying to evaluate the given pointer path. PointerPathEvaluation, + /// The target process is 32-bit, but the target memory address is not within the 32-bit address space. + /// + IncompatibleBitness, /// Failure when trying to read the given pointer. PointerReadFailure, /// The given pointer is a zero pointer. @@ -47,6 +50,20 @@ public override string ToString() => $"Failed to evaluate the specified pointer path: {Details}"; } +/// +/// Represents a failure in a string settings search operation when the target process is 32-bit, but the target memory +/// address is not within the 32-bit address space. +/// +/// Address that caused the failure. +public record FindStringSettingsFailureOnIncompatibleBitness(UIntPtr Address) + : FindStringSettingsFailure(FindStringSettingsFailureReason.IncompatibleBitness) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The address to read, {Address}, is a 64-bit address, but the target process is 32-bit."; +} + /// /// Represents a failure in a string settings search operation when failing to read the value of the given pointer. /// diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs index cd495ff..717f821 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs @@ -125,4 +125,30 @@ public class ProcessMemoryEvaluateTestX86 : ProcessMemoryEvaluateTest { /// Gets a boolean value defining which version of the target app is used. protected override bool Is64Bit => false; + + /// + /// Tests with a pointer path that starts with an address that is + /// not within the 32-bit address space. + /// Expect a . + /// + [Test] + public void EvaluateWithX64PathOnX86ProcessTest() + { + var result = TestProcessMemory!.EvaluateMemoryAddress("1000000000,4"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + } + + /// + /// Tests with a pointer path that starts at a module address, + /// with the maximum 32-bit offset added to it. + /// Expect a . + /// + [Test] + public void EvaluateWithX64ModuleOffsetOnX86ProcessTest() + { + var result = TestProcessMemory!.EvaluateMemoryAddress($"{MainModuleName}+FFFFFFFF"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index 4bd4d1f..6bce5cc 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -1334,4 +1334,95 @@ public class ProcessMemoryReadTestX86 : ProcessMemoryReadTest { /// Gets a boolean value defining which version of the target app is used. protected override bool Is64Bit => false; + + /// + /// Tests with a 64-bit address on a 32-bit process. + /// Expect the result to be a . + /// + [Test] + public void ReadBytesOnX86WithX64AddressTest() + { + var address = (ulong)uint.MaxValue + 1; + var result = TestProcessMemory!.ReadBytes((UIntPtr)address, 8); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnIncompatibleBitness))); + } + + /// + /// Tests with a 64-bit address on a 32-bit + /// process. Expect the result to be a . + /// + [Test] + public void ReadBytesPartialOnX86WithX64AddressTest() + { + var address = (ulong)uint.MaxValue + 1; + var result = TestProcessMemory!.ReadBytesPartial((UIntPtr)address, new byte[8], 8); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnIncompatibleBitness))); + } + + /// + /// Tests with a 64-bit address on a 32-bit process. + /// Expect the result to be a . + /// + [Test] + public void ReadGenericOnX86WithX64AddressTest() + { + var address = (ulong)uint.MaxValue + 1; + var result = TestProcessMemory!.Read((UIntPtr)address); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnIncompatibleBitness))); + } + + /// + /// Tests with a 64-bit address on a 32-bit process. + /// Expect the result to be a . + /// + [Test] + public void ReadObjectOnX86WithX64AddressTest() + { + var address = (ulong)uint.MaxValue + 1; + var result = TestProcessMemory!.Read(typeof(int), (UIntPtr)address); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnIncompatibleBitness))); + } + + /// + /// Tests with a 64-bit address on a 32-bit process. + /// Expect the result to be a . + /// + [Test] + public void FindStringSettingsOnX86WithX64AddressTest() + { + var address = (ulong)uint.MaxValue + 1; + var result = TestProcessMemory!.FindStringSettings((UIntPtr)address, "Whatever"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnIncompatibleBitness))); + } + + /// + /// Tests with a 64-bit address on a + /// 32-bit process. Expect the result to be a . + /// + [Test] + public void ReadRawStringOnX86WithX64AddressTest() + { + var address = (ulong)uint.MaxValue + 1; + var result = TestProcessMemory!.ReadRawString((UIntPtr)address, Encoding.Unicode); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnIncompatibleBitness))); + } + + /// + /// Tests with a 64-bit address on a 32-bit + /// process. Expect the result to be a . + /// + [Test] + public void ReadStringPointerOnX86WithX64AddressTest() + { + var address = (ulong)uint.MaxValue + 1; + var result = TestProcessMemory!.ReadStringPointer((UIntPtr)address, GetDotNetStringSettings()); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnIncompatibleBitness))); + } } \ No newline at end of file From 241b8a8e4da306f3dbe790f0fcbe7c7e0aebc3fd Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 6 Jul 2024 01:08:33 +0200 Subject: [PATCH 39/66] More x86-specific tests --- .../Code/ProcessMemory.Code.cs | 2 + .../Hooks/ProcessMemory.Hooks.cs | 8 ++ .../Results/CodeWritingFailure.cs | 17 ++++ src/MindControl.Code/Results/HookFailure.cs | 17 ++++ .../ProcessMemoryCodeExtensionsTest.cs | 13 +++ .../ProcessMemoryHookExtensionsTest.cs | 84 +++++++++++++++++++ .../ProcessMemoryInjectionTest.cs | 12 +++ .../ProcessMemoryWriteTest.cs | 40 +++++++++ 8 files changed, 193 insertions(+) diff --git a/src/MindControl.Code/Code/ProcessMemory.Code.cs b/src/MindControl.Code/Code/ProcessMemory.Code.cs index e46df9e..3363b9a 100644 --- a/src/MindControl.Code/Code/ProcessMemory.Code.cs +++ b/src/MindControl.Code/Code/ProcessMemory.Code.cs @@ -105,6 +105,8 @@ public static Result DisableCodeAt(this ProcessM return new CodeWritingFailureOnDetachedProcess(); if (address == UIntPtr.Zero) return new CodeWritingFailureOnZeroPointer(); + if (!processMemory.Is64Bit && address.ToUInt64() > uint.MaxValue) + return new CodeWritingFailureOnIncompatibleBitness(address); if (instructionCount < 1) return new CodeWritingFailureOnInvalidArguments( "The number of instructions to replace must be at least 1."); diff --git a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs index ad0b8b2..5e85aaa 100644 --- a/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs +++ b/src/MindControl.Code/Hooks/ProcessMemory.Hooks.cs @@ -82,6 +82,8 @@ public static Result Hook(this ProcessMemory processMemor return new HookFailureOnDetachedProcess(); if (targetInstructionAddress == UIntPtr.Zero) return new HookFailureOnZeroPointer(); + if (!processMemory.Is64Bit && targetInstructionAddress.ToUInt64() > uint.MaxValue) + return new HookFailureOnIncompatibleBitness(targetInstructionAddress); if (code.Length == 0) return new HookFailureOnInvalidArguments("The code to inject must contain at least one byte."); @@ -151,6 +153,8 @@ public static Result Hook(this ProcessMemory processMemor return new HookFailureOnDetachedProcess(); if (targetInstructionAddress == UIntPtr.Zero) return new HookFailureOnZeroPointer(); + if (!processMemory.Is64Bit && targetInstructionAddress.ToUInt64() > uint.MaxValue) + return new HookFailureOnIncompatibleBitness(targetInstructionAddress); if (codeAssembler.Instructions.Count == 0) return new HookFailureOnInvalidArguments("The given code assembler must have at least one instruction."); @@ -356,6 +360,8 @@ public static Result ReplaceCodeAt(this ProcessMemory p return new HookFailureOnDetachedProcess(); if (targetInstructionAddress == UIntPtr.Zero) return new HookFailureOnZeroPointer(); + if (!processMemory.Is64Bit && targetInstructionAddress.ToUInt64() > uint.MaxValue) + return new HookFailureOnIncompatibleBitness(targetInstructionAddress); if (instructionCount < 1) return new HookFailureOnInvalidArguments("The number of instructions to replace must be at least 1."); if (code.Length == 0) @@ -449,6 +455,8 @@ public static Result ReplaceCodeAt(this ProcessMemory p return new HookFailureOnDetachedProcess(); if (targetInstructionAddress == UIntPtr.Zero) return new HookFailureOnZeroPointer(); + if (!processMemory.Is64Bit && targetInstructionAddress.ToUInt64() > uint.MaxValue) + return new HookFailureOnIncompatibleBitness(targetInstructionAddress); if (instructionCount < 1) return new HookFailureOnInvalidArguments("The number of instructions to replace must be at least 1."); if (codeAssembler.Instructions.Count == 0) diff --git a/src/MindControl.Code/Results/CodeWritingFailure.cs b/src/MindControl.Code/Results/CodeWritingFailure.cs index 7a331fa..77aa2db 100644 --- a/src/MindControl.Code/Results/CodeWritingFailure.cs +++ b/src/MindControl.Code/Results/CodeWritingFailure.cs @@ -11,6 +11,9 @@ public enum CodeWritingFailureReason DetachedProcess, /// The given pointer path could not be successfully evaluated. PathEvaluationFailure, + /// The target process is 32-bit, but the target memory address is not within the 32-bit address space. + /// + IncompatibleBitness, /// The arguments provided to the code write operation are invalid. InvalidArguments, /// The target address is a zero pointer. @@ -52,6 +55,20 @@ public record CodeWritingFailureOnPathEvaluation(PathEvaluationFailure Details) public override string ToString() => $"Failed to evaluate the given pointer path: {Details}"; } +/// +/// Represents a failure that occurred while writing code to a target process when the target process is 32-bit, but the +/// target memory address is not within the 32-bit address space. +/// +/// Address that caused the failure. +public record CodeWritingFailureOnIncompatibleBitness(UIntPtr Address) + : CodeWritingFailure(CodeWritingFailureReason.IncompatibleBitness) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The address to read, {Address}, is a 64-bit address, but the target process is 32-bit."; +} + /// /// Represents a failure that occurred while writing code to a target process when the arguments provided are invalid. /// diff --git a/src/MindControl.Code/Results/HookFailure.cs b/src/MindControl.Code/Results/HookFailure.cs index ada3e24..eb4467f 100644 --- a/src/MindControl.Code/Results/HookFailure.cs +++ b/src/MindControl.Code/Results/HookFailure.cs @@ -11,6 +11,9 @@ public enum HookFailureReason DetachedProcess, /// The given pointer path could not be successfully evaluated. PathEvaluationFailure, + /// The target process is 32-bit, but the target memory address is not within the 32-bit address space. + /// + IncompatibleBitness, /// The target address is a zero pointer. ZeroPointer, /// The arguments provided to the hook operation are invalid. @@ -56,6 +59,20 @@ public record HookFailureOnPathEvaluation(PathEvaluationFailure Details) public override string ToString() => $"Failed to evaluate the given pointer path: {Details}"; } +/// +/// Represents a failure that occurred in a hook operation when the target process is 32-bit, but the target memory +/// address is not within the 32-bit address space. +/// +/// Address that caused the failure. +public record HookFailureOnIncompatibleBitness(UIntPtr Address) + : HookFailure(HookFailureReason.IncompatibleBitness) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"The address to read, {Address}, is a 64-bit address, but the target process is 32-bit."; +} + /// /// Represents a failure that occurred in a hook operation when the target address is a zero pointer. /// diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs index 609be9a..5fb6797 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs @@ -178,4 +178,17 @@ public class ProcessMemoryCodeExtensionsTestX86 : ProcessMemoryCodeExtensionsTes { /// Gets a boolean value defining which version of the target app is used. protected override bool Is64Bit => false; + + /// + /// Tests with an x64 address on + /// an x86 process. Expects a . + /// + [Test] + public void DisableCodeAtX64AddressOnX86ProcessTest() + { + var address = (ulong)uint.MaxValue + 1; + var result = TestProcessMemory!.DisableCodeAt(new UIntPtr(address)); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.TypeOf()); + } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs index aaee9f0..c378602 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs @@ -763,4 +763,88 @@ protected override Assembler AssembleRcxMov(uint value) assembler.mov(ecx, value); return assembler; } + + /// + /// Tests with a 64-bit + /// address on a 32-bit process. Expects a result. + /// + [Test] + public void HookWithByteArrayOnX64AddressOnX86ProcessTest() + { + var address = (ulong)uint.MaxValue + 1; + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + var hookResult = TestProcessMemory!.Hook((UIntPtr)address, bytes, + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests with a 64-bit + /// address on a 32-bit process. Expects a result. + /// + [Test] + public void HookWithAssemblerOnX64AddressOnX86ProcessTest() + { + var address = (ulong)uint.MaxValue + 1; + var hookResult = TestProcessMemory!.Hook((UIntPtr)address, AssembleAlternativeMovInt(), + new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests with a + /// 64-bit address on a 32-bit process. Expects a result. + /// + [Test] + public void InsertCodeAtWithByteArrayOnX64AddressOnX86ProcessTest() + { + var address = (ulong)uint.MaxValue + 1; + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + var hookResult = TestProcessMemory!.InsertCodeAt((UIntPtr)address, bytes); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests + /// with a 64-bit address on a 32-bit process. Expects a result. + /// + [Test] + public void InsertCodeAtWithAssemblerOnX64AddressOnX86ProcessTest() + { + var address = (ulong)uint.MaxValue + 1; + var hookResult = TestProcessMemory!.InsertCodeAt((UIntPtr)address, AssembleAlternativeMovInt()); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests + /// with a 64-bit address on a 32-bit process. Expects a result. + /// + [Test] + public void ReplaceCodeAtWithByteArrayOnX64AddressOnX86ProcessTest() + { + var address = (ulong)uint.MaxValue + 1; + var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; + var hookResult = TestProcessMemory!.ReplaceCodeAt((UIntPtr)address, 1, bytes); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } + + /// + /// Tests + /// + /// with a 64-bit address on a 32-bit process. Expects a result. + /// + [Test] + public void ReplaceCodeAtWithAssemblerOnX64AddressOnX86ProcessTest() + { + var address = (ulong)uint.MaxValue + 1; + var hookResult = TestProcessMemory!.ReplaceCodeAt((UIntPtr)address, 1, AssembleAlternativeMovInt()); + Assert.That(hookResult.IsSuccess, Is.False); + Assert.That(hookResult.Error, Is.TypeOf()); + } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs index 3d8be0e..d751099 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs @@ -105,4 +105,16 @@ public class ProcessMemoryInjectionTestX86 : ProcessMemoryInjectionTest { /// Gets a boolean value defining which version of the target app is used. protected override bool Is64Bit => false; + + /// + /// Tests with a 64-bit library on a 32-bit process. + /// Expect a . + /// + [Test] + public void InjectX64LibraryOnX86ProcessTest() + { + var result = TestProcessMemory!.InjectLibrary(GetInjectedLibraryPath(true)); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs index 852fece..37fd387 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs @@ -173,4 +173,44 @@ public class ProcessMemoryWriteTestX86 : ProcessMemoryWriteTest { /// Gets a boolean value defining which version of the target app is used. protected override bool Is64Bit => false; + + /// + /// Tests on a 32-bit target app + /// with a 64-bit address. Expect a error. + /// + [Test] + public void WriteGenericOnX86WithX64AddressTest() + { + var address = (ulong)uint.MaxValue + 1; + var result = TestProcessMemory!.Write((UIntPtr)address, 8); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// + /// Tests on a 32-bit + /// target app with a 64-bit address. Expect a error. + /// + [Test] + public void WriteBytesOnX86WithX64AddressTest() + { + var address = (ulong)uint.MaxValue + 1; + var result = TestProcessMemory!.WriteBytes((UIntPtr)address, new byte[8]); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// + /// Tests on a 32-bit target app + /// with a reachable address, but a value with a pointer type that goes beyond the 32-bit address space. + /// Expect a error. + /// + [Test] + public void WriteX64PointerOnX86Test() + { + var address = (ulong)uint.MaxValue + 1; + var result = TestProcessMemory!.Write(OuterClassPointer, (UIntPtr)address); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } } \ No newline at end of file From 2d8bbfedaa4dce5b45ebd28c8efa00a0fbc7a7ca Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 6 Jul 2024 01:17:57 +0200 Subject: [PATCH 40/66] Removed unused Win32 function calls --- src/MindControl/Native/IOperatingSystemService.cs | 11 ----------- src/MindControl/Native/Win32Service.cs | 12 ------------ 2 files changed, 23 deletions(-) diff --git a/src/MindControl/Native/IOperatingSystemService.cs b/src/MindControl/Native/IOperatingSystemService.cs index bfc4344..3a38539 100644 --- a/src/MindControl/Native/IOperatingSystemService.cs +++ b/src/MindControl/Native/IOperatingSystemService.cs @@ -95,12 +95,6 @@ Result AllocateMemory(IntPtr processHandle, int size, Me /// A result holding either a pointer to the start of the allocated memory, or a system failure. Result AllocateMemory(IntPtr processHandle, UIntPtr address, int size, MemoryAllocationType allocationType, MemoryProtection protection); - - /// - /// Gets the address of the function used to load a library in the current process. - /// - /// A result holding either the address of the function, or a system failure. - Result GetLoadLibraryFunctionAddress(); /// /// Spawns a thread in the specified process, starting at the given address. @@ -150,11 +144,6 @@ Result CreateRemoteThread(IntPtr processHandle, UIntPtr s /// A result holding either the metadata of the target memory region, or a system failure. Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress); - /// - /// Gets the allocation granularity (minimal allocation size) of the system. - /// - uint GetAllocationGranularity(); - /// /// Gets the page size of the system. /// diff --git a/src/MindControl/Native/Win32Service.cs b/src/MindControl/Native/Win32Service.cs index c954b76..924bbdc 100644 --- a/src/MindControl/Native/Win32Service.cs +++ b/src/MindControl/Native/Win32Service.cs @@ -326,13 +326,6 @@ private Result GetFunctionAddress(string moduleName, str return functionAddress; } - /// - /// Gets the address of the function used to load a library in the current process. - /// - /// A result holding either the address of the function, or a system failure. - public Result GetLoadLibraryFunctionAddress() - => GetFunctionAddress("kernel32.dll", "LoadLibraryW"); - /// /// Spawns a thread in the specified process, starting at the given address. /// @@ -441,11 +434,6 @@ public MemoryRange GetFullMemoryRange() return new MemoryRange(systemInfo.MinimumApplicationAddress, systemInfo.MaximumApplicationAddress); } - /// - /// Gets the allocation granularity (minimal allocation size) of the system. - /// - public uint GetAllocationGranularity() => GetSystemInfo().AllocationGranularity; - /// /// Gets the page size of the system. /// From dad455b6a2bcf05d69c5db0945b43d1bc22b4d67 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 6 Jul 2024 12:55:13 +0200 Subject: [PATCH 41/66] Attach methods use Result --- .../ProcessMemory/ProcessMemory.cs | 140 ++++++++++-------- src/MindControl/ProcessTracker.cs | 6 +- src/MindControl/Results/AttachFailure.cs | 82 ++++++++++ src/MindControl/Results/ReadFailure.cs | 3 +- .../BaseProcessMemoryTest.cs | 2 +- .../ProcessMemoryAttachTest.cs | 34 +++++ 6 files changed, 197 insertions(+), 70 deletions(-) create mode 100644 src/MindControl/Results/AttachFailure.cs diff --git a/src/MindControl/ProcessMemory/ProcessMemory.cs b/src/MindControl/ProcessMemory/ProcessMemory.cs index 89ae8bc..f6b6bb4 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using MindControl.Native; +using MindControl.Results; namespace MindControl; @@ -24,13 +25,13 @@ public partial class ProcessMemory : IDisposable /// /// Gets a boolean indicating if the process is 64-bit. /// - public bool Is64Bit { get; private set; } + public bool Is64Bit { get; } /// /// Gets the handle of the attached process. /// Use this if you need to manually call Win32 API functions. /// - public IntPtr ProcessHandle { get; private set; } + public IntPtr ProcessHandle { get; } /// /// Gets or sets the default way this instance deals with memory protection. @@ -46,24 +47,26 @@ public partial class ProcessMemory : IDisposable /// /// Attaches to a process with the given name and returns the resulting instance. - /// If multiple processes with the specified name are running, one of them will be targeted arbitrarily. + /// If multiple processes with the specified name are running, a + /// will be returned. /// When there is any risk of this happening, it is recommended to use instead. /// /// Name of the process to open. - /// The attached process instance resulting from the operation. - /// Thrown when no running process with the given name can be found. - public static ProcessMemory OpenProcessByName(string processName) + /// A result holding either the attached process instance, or an error. + public static Result OpenProcessByName(string processName) { var matches = Process.GetProcessesByName(processName); if (matches.Length == 0) - throw new ProcessException($"No running process with the name \"{processName}\" could be found."); - - // If we have multiple results, we need to dispose those that we will not be using. - // Arbitrarily, we will use the first result. So we dispose everything starting from index 1. - for (var i = 1; i < matches.Length; i++) - matches[i].Dispose(); + return new AttachFailureOnTargetProcessNotFound(); + if (matches.Length > 1) + { + var pids = matches.Select(p => p.Id).ToArray(); + foreach (var process in matches) + process.Dispose(); + return new AttachFailureOnMultipleTargetProcessesFound(pids); + } - return OpenProcess(matches.First(), true); + return OpenProcess(matches.First(), true, new Win32Service()); } /// @@ -71,21 +74,39 @@ public static ProcessMemory OpenProcessByName(string processName) /// instance. /// /// Identifier of the process to attach to. - /// The attached process instance resulting from the operation. - public static ProcessMemory OpenProcessById(int pid) + /// A result holding either the attached process instance, or an error. + public static Result OpenProcessById(int pid) { - var process = Process.GetProcessById(pid); - if (process == null) - throw new ProcessException(pid, $"No running process with the PID {pid} could be found."); - return OpenProcess(process, true); + try + { + var process = Process.GetProcessById(pid); + return OpenProcess(process, true, new Win32Service()); + } + catch (ArgumentException) + { + // Process.GetProcessById throws an ArgumentException when the PID does not match any process. + return new AttachFailureOnTargetProcessNotFound(); + } } /// /// Attaches to the given process, and returns the resulting instance. /// /// Process to attach to. - /// The attached process instance resulting from the operation. - public static ProcessMemory OpenProcess(Process target) => OpenProcess(target, false); + /// A result holding either the attached process instance, or an error. + public static Result OpenProcess(Process target) + => OpenProcess(target, false, new Win32Service()); + + /// + /// Attaches to the given process, and returns the resulting instance. This variant + /// allows you to specify an implementation of to use instead of the default + /// implementation. Unless you have very specific needs, use another overload of this method. + /// + /// Process to attach to. + /// Service that provides system-specific process-oriented features. + /// A result holding either the attached process instance, or an error. + public static Result OpenProcess(Process target, IOperatingSystemService osService) + => OpenProcess(target, false, osService); /// /// Attaches to the given process, and returns the resulting instance. @@ -93,23 +114,31 @@ public static ProcessMemory OpenProcessById(int pid) /// Process to attach to. /// Indicates if this instance should take ownership of the /// , meaning it has the responsibility to dispose it. - /// The attached process instance resulting from the operation. - private static ProcessMemory OpenProcess(Process target, bool ownsProcessInstance) + /// Service that provides system-specific process-oriented features. + /// A result holding either the attached process instance, or an error. + internal static Result OpenProcess(Process target, bool ownsProcessInstance, + IOperatingSystemService osService) { if (target.HasExited) - throw new ProcessException(target.Id, $"Process {target.Id} has exited."); - - return new ProcessMemory(target, ownsProcessInstance); + return new AttachFailureOnTargetProcessNotRunning(); + + // Determine target bitness + var is64BitResult = osService.IsProcess64Bit(target.Id); + if (is64BitResult.IsFailure) + return new AttachFailureOnSystemError(is64BitResult.Error); + var is64Bit = is64BitResult.Value; + if (is64Bit && IntPtr.Size != 8) + return new AttachFailureOnIncompatibleBitness(); + + // Open the process with the required access flags + var openResult = osService.OpenProcess(target.Id); + if (openResult.IsFailure) + return new AttachFailureOnSystemError(openResult.Error); + var processHandle = openResult.Value; + + // Build the instance with the handle and bitness information + return new ProcessMemory(target, ownsProcessInstance, processHandle, is64Bit, osService); } - - /// - /// Builds a new instance that attaches to the given process. - /// - /// Target process. - /// Indicates if this instance should take ownership of the - /// , meaning it has the responsibility to dispose it. - public ProcessMemory(Process process, bool ownsProcessInstance) - : this(process, ownsProcessInstance, new Win32Service()) {} /// /// Builds a new instance that attaches to the given process. @@ -119,40 +148,23 @@ public ProcessMemory(Process process, bool ownsProcessInstance) /// Target process. /// Indicates if this instance should take ownership of the /// , meaning it has the responsibility to dispose it. + /// Handle of the target process, open with specific access flags to allow memory + /// manipulation. + /// Indicates if the target process is 64-bit. /// Service that provides system-specific process-oriented features. - private ProcessMemory(Process process, bool ownsProcessInstance, IOperatingSystemService osService) + private ProcessMemory(Process process, bool ownsProcessInstance, IntPtr processHandle, bool is64Bit, + IOperatingSystemService osService) { _process = process; _osService = osService; _ownsProcessInstance = ownsProcessInstance; - Attach(); - } - - /// - /// Attaches to the process. - /// - private void Attach() - { - try - { - Is64Bit = _osService.IsProcess64Bit(_process.Id).GetValueOrDefault(defaultValue: true); - - if (Is64Bit && !Environment.Is64BitOperatingSystem) - throw new ProcessException(_process.Id, "A 32-bit program cannot attach to a 64-bit process."); - - _process.EnableRaisingEvents = true; - _process.Exited += OnProcessExited; - var openResult = _osService.OpenProcess(_process.Id); - openResult.ThrowOnError(); - ProcessHandle = openResult.Value; - - IsAttached = true; - } - catch (Exception e) - { - Detach(); - throw new ProcessException(_process.Id, $"Failed to attach to the process {_process.Id}. Check the internal exception for more information. Common causes include insufficient privileges (administrator rights might be required) or trying to attach to a x64 process with a x86 program.", e); - } + ProcessHandle = processHandle; + Is64Bit = is64Bit; + IsAttached = true; + + // Make sure this instance gets notified when the process exits + _process.EnableRaisingEvents = true; + _process.Exited += OnProcessExited; } /// diff --git a/src/MindControl/ProcessTracker.cs b/src/MindControl/ProcessTracker.cs index f35d8bb..2fd752e 100644 --- a/src/MindControl/ProcessTracker.cs +++ b/src/MindControl/ProcessTracker.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using MindControl.Native; namespace MindControl; @@ -107,7 +108,8 @@ private void Detach() private ProcessMemory? AttemptToAttachProcess() { var process = GetTargetProcess(); - return process == null ? null : new ProcessMemory(process, ownsProcessInstance: true); + return process == null ? null : + ProcessMemory.OpenProcess(process, true, new Win32Service()).GetValueOrDefault(); } /// @@ -127,9 +129,7 @@ private void Detach() // If we found multiple processes, take one (arbitrarily, the lowest by ID) and dispose the others. var target = processes.MinBy(p => p.Id); foreach (var process in processes.Except(new [] { target })) - { process?.Dispose(); - } return target; } } diff --git a/src/MindControl/Results/AttachFailure.cs b/src/MindControl/Results/AttachFailure.cs new file mode 100644 index 0000000..2ed9261 --- /dev/null +++ b/src/MindControl/Results/AttachFailure.cs @@ -0,0 +1,82 @@ +namespace MindControl.Results; + +/// Represents a reason for a process attach operation to fail. +public enum AttachFailureReason +{ + /// The process with the given name or PID could not be found. + TargetProcessNotFound, + /// Multiple processes with the given name were found. + MultipleTargetProcessesFound, + /// The target process instance is not running. + TargetProcessNotRunning, + /// Attempting to attach to a 64-bit process with a 32-bit process. + IncompatibleBitness, + /// A system call involved in the process attaching operation has failed. + SystemError, +} + +/// +/// Represents a failure in a process attach operation. +/// +/// Reason for the failure. +public abstract record AttachFailure(AttachFailureReason Reason); + +/// +/// Represents a failure in a process attach operation when the target process could not be found. +/// +public record AttachFailureOnTargetProcessNotFound() + : AttachFailure(AttachFailureReason.TargetProcessNotFound) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => "The target process could not be found. Make sure the process is running."; +} + +/// +/// Represents a failure in a process attach operation when multiple processes with the given name were found. +/// +/// Identifiers of running processes with the given name. +public record AttachFailureOnMultipleTargetProcessesFound(int[] Pids) + : AttachFailure(AttachFailureReason.MultipleTargetProcessesFound) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => "Multiple processes with the given name were found. Use a PID to disambiguate."; +} + +/// +/// Represents a failure in a process attach operation when the target process is not running. +/// +public record AttachFailureOnTargetProcessNotRunning() + : AttachFailure(AttachFailureReason.TargetProcessNotRunning) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => "The target process is not running."; +} + +/// +/// Represents a failure in a process attach operation when the target process is 64-bit and the current process is +/// 32-bit. +/// +public record AttachFailureOnIncompatibleBitness() + : AttachFailure(AttachFailureReason.IncompatibleBitness) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => "Cannot open a 64-bit process from a 32-bit process."; +} + +/// +/// Represents a failure in a process attach operation when a system error occurs. +/// +/// Details about the system error that occurred. +public record AttachFailureOnSystemError(SystemFailure Details) + : AttachFailure(AttachFailureReason.SystemError) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + => $"A system error occurred while attempting to attach to the process: {Details}"; +} \ No newline at end of file diff --git a/src/MindControl/Results/ReadFailure.cs b/src/MindControl/Results/ReadFailure.cs index 39ffab4..1dca9dc 100644 --- a/src/MindControl/Results/ReadFailure.cs +++ b/src/MindControl/Results/ReadFailure.cs @@ -99,8 +99,7 @@ public record ReadFailureOnSystemRead(SystemFailure Details) { /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() - => $"Failed to read at the target address: {Details}"; + public override string ToString() => $"Failed to read at the target address: {Details}"; } /// diff --git a/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs b/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs index fe4cf58..c4359a1 100644 --- a/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/BaseProcessMemoryTest.cs @@ -41,7 +41,7 @@ public void Initialize() if (!UIntPtr.TryParse(line, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out OuterClassPointer)) throw new Exception($"Could not read the outer class pointer output by the app: \"{line}\"."); - TestProcessMemory = ProcessMemory.OpenProcess(_targetProcess); + TestProcessMemory = ProcessMemory.OpenProcess(_targetProcess).Value; } /// diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs index 8ceae48..455ce66 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs @@ -1,9 +1,43 @@ using NUnit.Framework; +using MindControl.Results; namespace MindControl.Test.ProcessMemoryTests; /// /// Tests the features of the class related to attaching to a process. +/// This class does not inherit from and thus does not start a target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryInstancelessAttachTest +{ + /// + /// Tests when no process with the given name is found. + /// Expects a result. + /// + [Test] + public void OpenProcessByNameWithNoMatchTest() + { + var result = ProcessMemory.OpenProcessByName("ThisProcessDoesNotExist"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } + + /// + /// Tests with a PID that does not match any running process. + /// Expects a result. + /// + [Test] + public void OpenProcessByInvalidPidTest() + { + var result = ProcessMemory.OpenProcessById(-1); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.Error, Is.InstanceOf()); + } +} + +/// +/// Tests the features of the class related to attaching to a process. +/// This class inherits from and thus does start a target app. /// [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ProcessMemoryAttachTest : BaseProcessMemoryTest From a6735f5ca060aace192e7312816055a2ff62b8d2 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Fri, 12 Jul 2024 17:06:10 +0200 Subject: [PATCH 42/66] Tentative ValueAnchor implementation --- .../Addressing/IAddressResolver.cs | 16 +++ .../Addressing/LiteralAddressResolver.cs | 26 ++++ .../Addressing/PointerPathResolver.cs | 22 ++++ .../Anchors/ByteArrayMemoryAdapter.cs | 59 +++++++++ .../Anchors/GenericMemoryAdapter.cs | 40 ++++++ src/MindControl/Anchors/IMemoryAdapter.cs | 23 ++++ src/MindControl/Anchors/IValueAnchor.cs | 7 ++ .../Anchors/StringPointerMemoryAdapter.cs | 38 ++++++ src/MindControl/Anchors/ValueAnchor.cs | 29 +++++ .../ProcessMemory/ProcessMemory.Anchors.cs | 114 ++++++++++++++++++ .../ProcessMemory/ProcessMemory.cs | 4 + src/MindControl/Results/AllocationFailure.cs | 41 ++----- src/MindControl/Results/AttachFailure.cs | 49 ++------ src/MindControl/Results/FindBytesFailure.cs | 4 +- .../Results/FindStringSettingsFailure.cs | 47 ++------ src/MindControl/Results/InjectionFailure.cs | 64 +++------- .../Results/NotSupportedFailure.cs | 13 ++ .../Results/PathEvaluationFailure.cs | 40 ++---- src/MindControl/Results/ReadFailure.cs | 91 +++++--------- src/MindControl/Results/ReservationFailure.cs | 27 +---- src/MindControl/Results/StoreFailure.cs | 33 +---- src/MindControl/Results/StringReadFailure.cs | 88 ++++---------- src/MindControl/Results/SystemFailure.cs | 37 +----- src/MindControl/Results/ThreadFailure.cs | 80 +++--------- src/MindControl/Results/WriteFailure.cs | 93 +++++--------- 25 files changed, 561 insertions(+), 524 deletions(-) create mode 100644 src/MindControl/Addressing/IAddressResolver.cs create mode 100644 src/MindControl/Addressing/LiteralAddressResolver.cs create mode 100644 src/MindControl/Addressing/PointerPathResolver.cs create mode 100644 src/MindControl/Anchors/ByteArrayMemoryAdapter.cs create mode 100644 src/MindControl/Anchors/GenericMemoryAdapter.cs create mode 100644 src/MindControl/Anchors/IMemoryAdapter.cs create mode 100644 src/MindControl/Anchors/IValueAnchor.cs create mode 100644 src/MindControl/Anchors/StringPointerMemoryAdapter.cs create mode 100644 src/MindControl/Anchors/ValueAnchor.cs create mode 100644 src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs create mode 100644 src/MindControl/Results/NotSupportedFailure.cs diff --git a/src/MindControl/Addressing/IAddressResolver.cs b/src/MindControl/Addressing/IAddressResolver.cs new file mode 100644 index 0000000..f914f99 --- /dev/null +++ b/src/MindControl/Addressing/IAddressResolver.cs @@ -0,0 +1,16 @@ +using MindControl.Results; + +namespace MindControl; + +/// +/// Provides a way to resolve an address in the target process. +/// +public interface IAddressResolver +{ + /// + /// Resolves the address in the target process using the given instance. + /// + /// Instance of attached to the target process. + /// A result holding either the resolved address, or a failure. + Result ResolveFor(ProcessMemory processMemory); +} \ No newline at end of file diff --git a/src/MindControl/Addressing/LiteralAddressResolver.cs b/src/MindControl/Addressing/LiteralAddressResolver.cs new file mode 100644 index 0000000..5ac744f --- /dev/null +++ b/src/MindControl/Addressing/LiteralAddressResolver.cs @@ -0,0 +1,26 @@ +using MindControl.Results; + +namespace MindControl; + +/// +/// Provides a way to resolve an address in the target process. +/// This implementation takes a literal address and always resolves to that same address. +/// +/// Literal address to return in . +public class LiteralAddressResolver(UIntPtr address) : IAddressResolver +{ + /// Gets the literal address to return in . + public UIntPtr Address => address; + + /// + /// Resolves the address in the target process using the given instance. + /// + /// Instance of attached to the target process. + /// A result holding either the resolved address, or a failure expressed as a string. + public Result ResolveFor(ProcessMemory processMemory) + { + if (!processMemory.Is64Bit && Address > uint.MaxValue) + return "Failed to resolve: the address is a 64-bit address, but the target process is 32-bit."; + return Address; + } +} \ No newline at end of file diff --git a/src/MindControl/Addressing/PointerPathResolver.cs b/src/MindControl/Addressing/PointerPathResolver.cs new file mode 100644 index 0000000..1d77df0 --- /dev/null +++ b/src/MindControl/Addressing/PointerPathResolver.cs @@ -0,0 +1,22 @@ +using MindControl.Results; + +namespace MindControl; + +/// +/// Provides a way to resolve an address in the target process. +/// This implementation takes a pointer path and resolves it to an address in the target process. +/// +/// +public class PointerPathResolver(PointerPath pointerPath) : IAddressResolver +{ + /// Gets the pointer path to resolve. + public PointerPath PointerPath { get; } = pointerPath; + + /// + /// Evaluates the pointer path in the target process using the given instance. + /// + /// Instance of attached to the target process. + /// A result holding either the resolved address, or a failure. + public Result ResolveFor(ProcessMemory processMemory) => + processMemory.EvaluateMemoryAddress(PointerPath); +} \ No newline at end of file diff --git a/src/MindControl/Anchors/ByteArrayMemoryAdapter.cs b/src/MindControl/Anchors/ByteArrayMemoryAdapter.cs new file mode 100644 index 0000000..e048b3b --- /dev/null +++ b/src/MindControl/Anchors/ByteArrayMemoryAdapter.cs @@ -0,0 +1,59 @@ +using MindControl.Results; + +namespace MindControl.Anchors; + +/// +/// Represents an adapter for reading and writing a value from and to memory. +/// This implementation reads and writes a byte array from and to memory using an address resolver and a size. +/// +/// +public class ByteArrayMemoryAdapter : IMemoryAdapter +{ + private readonly IAddressResolver _addressResolver; + private readonly int _size; + + /// + /// Represents an adapter for reading and writing a value from and to memory. + /// This implementation reads and writes a byte array from and to memory using an address resolver and a size. + /// + /// Resolver that provides the address of the array in memory. + /// Size of the byte array to read and write. + /// + public ByteArrayMemoryAdapter(IAddressResolver addressResolver, int size) + { + if (size <= 0) + throw new ArgumentOutOfRangeException(nameof(size), "The size must be greater than zero."); + + _addressResolver = addressResolver; + _size = size; + } + + /// Reads the value in the memory of the target process. + /// Instance of attached to the target process. + /// A result holding either the value read from memory, or a failure. + public Result Read(ProcessMemory processMemory) + { + var addressResult = _addressResolver.ResolveFor(processMemory); + if (addressResult.IsFailure) + return new ReadFailureOnAddressResolution(addressResult.Error); + + return processMemory.ReadBytes(addressResult.Value, _size); + } + + /// Writes the value to the memory of the target process. + /// Instance of attached to the target process. + /// Value to write to memory. + /// A result indicating success or failure. + public Result Write(ProcessMemory processMemory, byte[] value) + { + if (value.Length != _size) + return new WriteFailureOnInvalidArguments( + $"The size of the byte array does not match the expected size of {_size}."); + + var addressResult = _addressResolver.ResolveFor(processMemory); + if (addressResult.IsFailure) + return new WriteFailureOnAddressResolution(addressResult.Error); + + return processMemory.WriteBytes(addressResult.Value, value); + } +} \ No newline at end of file diff --git a/src/MindControl/Anchors/GenericMemoryAdapter.cs b/src/MindControl/Anchors/GenericMemoryAdapter.cs new file mode 100644 index 0000000..a18d021 --- /dev/null +++ b/src/MindControl/Anchors/GenericMemoryAdapter.cs @@ -0,0 +1,40 @@ +using MindControl.Results; + +namespace MindControl.Anchors; + +/// +/// Represents an adapter for reading and writing a value from and to memory. +/// This implementation reads and writes any value type from and to memory using an address resolver. +/// +/// Resolver that provides the address of the value in memory. +/// Type of the value to read and write. +/// Type of the failure that can occur when resolving the address of the value. +/// +public class GenericMemoryAdapter(IAddressResolver addressResolver) + : IMemoryAdapter where TValue : struct +{ + /// Reads the value in the memory of the target process. + /// Instance of attached to the target process. + /// A result holding either the value read from memory, or a failure. + public Result Read(ProcessMemory processMemory) + { + var addressResult = addressResolver.ResolveFor(processMemory); + if (addressResult.IsFailure) + return new ReadFailureOnAddressResolution(addressResult.Error); + + return processMemory.Read(addressResult.Value); + } + + /// Writes the value to the memory of the target process. + /// Instance of attached to the target process. + /// Value to write to memory. + /// A result indicating success or failure. + public Result Write(ProcessMemory processMemory, TValue value) + { + var addressResult = addressResolver.ResolveFor(processMemory); + if (addressResult.IsFailure) + return new WriteFailureOnAddressResolution(addressResult.Error); + + return processMemory.Write(addressResult.Value, value); + } +} \ No newline at end of file diff --git a/src/MindControl/Anchors/IMemoryAdapter.cs b/src/MindControl/Anchors/IMemoryAdapter.cs new file mode 100644 index 0000000..6965c4a --- /dev/null +++ b/src/MindControl/Anchors/IMemoryAdapter.cs @@ -0,0 +1,23 @@ +using MindControl.Results; + +namespace MindControl.Anchors; + +/// +/// Represents an adapter for reading and writing a value from and to memory. +/// +/// Type of the value to read and write. +/// Type of the failure that can occur when reading the value. +/// Type of the failure that can occur when writing the value. +public interface IMemoryAdapter +{ + /// Reads the value in the memory of the target process. + /// Instance of attached to the target process. + /// A result holding either the value read from memory, or a failure. + Result Read(ProcessMemory processMemory); + + /// Writes the value to the memory of the target process. + /// Instance of attached to the target process. + /// Value to write to memory. + /// A result indicating success or failure. + Result Write(ProcessMemory processMemory, TValue value); +} \ No newline at end of file diff --git a/src/MindControl/Anchors/IValueAnchor.cs b/src/MindControl/Anchors/IValueAnchor.cs new file mode 100644 index 0000000..ce9dcf4 --- /dev/null +++ b/src/MindControl/Anchors/IValueAnchor.cs @@ -0,0 +1,7 @@ +namespace MindControl.Anchors; + +/// Provides methods to manipulate and track a specific value in memory. +public interface IValueAnchor : IDisposable +{ + +} \ No newline at end of file diff --git a/src/MindControl/Anchors/StringPointerMemoryAdapter.cs b/src/MindControl/Anchors/StringPointerMemoryAdapter.cs new file mode 100644 index 0000000..de2f06f --- /dev/null +++ b/src/MindControl/Anchors/StringPointerMemoryAdapter.cs @@ -0,0 +1,38 @@ +using MindControl.Results; + +namespace MindControl.Anchors; + +/// +/// Represents an adapter for reading and writing a value from and to memory. +/// This implementation reads and writes a string from and to memory using an address resolver and the string settings. +/// +/// Resolver that provides the address of the string in memory. +/// Settings that define how the string is read and written. +/// Type of the failure that can occur when resolving the address of the string. +/// +public class StringPointerMemoryAdapter(IAddressResolver addressResolver, + StringSettings stringSettings) : IMemoryAdapter +{ + /// Reads the value in the memory of the target process. + /// Instance of attached to the target process. + /// A result holding either the value read from memory, or a failure. + public Result Read(ProcessMemory processMemory) + { + var addressResult = addressResolver.ResolveFor(processMemory); + if (addressResult.IsFailure) + return new StringReadFailureOnAddressResolution(addressResult.Error); + + return processMemory.ReadStringPointer(addressResult.Value, stringSettings); + } + + /// Writes the value to the memory of the target process. + /// Instance of attached to the target process. + /// Value to write to memory. + /// A result indicating success or failure. + public Result Write(ProcessMemory processMemory, string value) + { + // Not supported for now, might be in the future if we implement internal string instance management. + return new NotSupportedFailure( + "Writing a string to memory is not supported in this context, because it involves memory allocations that must be handled separately."); + } +} \ No newline at end of file diff --git a/src/MindControl/Anchors/ValueAnchor.cs b/src/MindControl/Anchors/ValueAnchor.cs new file mode 100644 index 0000000..e48ef58 --- /dev/null +++ b/src/MindControl/Anchors/ValueAnchor.cs @@ -0,0 +1,29 @@ +using MindControl.Results; + +namespace MindControl.Anchors; + +/// Provides methods to manipulate and track a specific value in memory. +/// Adapter that reads and writes the value from and to memory. +/// Instance of attached to the target process. +/// Type of the value to read and write. +/// Type of the failure that can occur when reading the value. +/// Type of the failure that can occur when writing the value. +public class ValueAnchor + (IMemoryAdapter memoryAdapter, ProcessMemory processMemory) + : IValueAnchor +{ + /// Reads the value in the memory of the target process. + /// A result holding either the value read from memory, or a failure. + public Result Read() => memoryAdapter.Read(processMemory); + + /// Writes the value to the memory of the target process. + /// Value to write to memory. + /// A result indicating success or failure. + public Result Write(TValue value) => memoryAdapter.Write(processMemory, value); + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + processMemory.RemoveAnchor(this); + } +} \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs b/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs new file mode 100644 index 0000000..8476a5c --- /dev/null +++ b/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs @@ -0,0 +1,114 @@ +using MindControl.Anchors; +using MindControl.Results; + +namespace MindControl; + +// This partial class implements methods related to anchors +public partial class ProcessMemory +{ + private readonly List _anchors = new(); + + /// + /// Builds and returns an anchor for a value of type at the specified address. Anchors + /// allow you to track and manipulate a specific value in memory. When not needed anymore, anchors should be + /// disposed. + /// + /// Address of the value in memory. + /// Type of the value to read and write. + /// An anchor for the value at the specified address. + public ValueAnchor RegisterAnchor(UIntPtr address) + where T : struct + { + var memoryAdapter = new GenericMemoryAdapter(new LiteralAddressResolver(address)); + var anchor = new ValueAnchor(memoryAdapter, this); + _anchors.Add(anchor); + return anchor; + } + + /// + /// Builds and returns an anchor for a value of type at the address referred by the given + /// pointer path. Anchors allow you to track and manipulate a specific value in memory. When not needed anymore, + /// anchors should be disposed. + /// + /// Pointer path to the address of the value in memory. + /// Type of the value to read and write. + /// An anchor for the value at the specified address. + public ValueAnchor RegisterAnchor(PointerPath pointerPath) + where T : struct + { + var memoryAdapter = new GenericMemoryAdapter(new PointerPathResolver(pointerPath)); + var anchor = new ValueAnchor(memoryAdapter, this); + _anchors.Add(anchor); + return anchor; + } + + /// + /// Builds and returns an anchor for a byte array at the specified address. Anchors allow you to track and + /// manipulate a specific value in memory. When not needed anymore, anchors should be disposed. + /// + /// Address of target byte array in memory. + /// Size of the target byte array. + /// An anchor for the array at the specified address. + public ValueAnchor RegisterByteArrayAnchor(UIntPtr address, int size) + { + var memoryAdapter = new ByteArrayMemoryAdapter(new LiteralAddressResolver(address), size); + var anchor = new ValueAnchor(memoryAdapter, this); + _anchors.Add(anchor); + return anchor; + } + + /// + /// Builds and returns an anchor for a byte array at the address referred by the given pointer path. Anchors allow + /// you to track and manipulate a specific value in memory. When not needed anymore, anchors should be disposed. + /// + /// Pointer path to the address of the target array in memory. + /// Size of the target byte array. + /// An anchor for the array at the specified address. + public ValueAnchor RegisterByteArrayAnchor(PointerPath pointerPath, int size) + { + var memoryAdapter = new ByteArrayMemoryAdapter( + new PointerPathResolver(pointerPath), size); + var anchor = new ValueAnchor(memoryAdapter, this); + _anchors.Add(anchor); + return anchor; + } + + /// + /// Builds and returns an anchor for a string pointer at the specified address. Anchors allow you to track and + /// manipulate a specific value in memory. When not needed anymore, anchors should be disposed. + /// String anchors are read-only. To write strings, please see the documentation. + /// + /// Address of the string pointer in memory. + /// Settings to read the string. + /// An anchor for the value at the specified address. + public ValueAnchor RegisterStringAnchor(UIntPtr address, + StringSettings stringSettings) + { + var memoryAdapter = new StringPointerMemoryAdapter(new LiteralAddressResolver(address), stringSettings); + var anchor = new ValueAnchor(memoryAdapter, this); + _anchors.Add(anchor); + return anchor; + } + + /// + /// Builds and returns an anchor for a string pointer at the address referred by the specified pointer path. Anchors + /// allow you to track and manipulate a specific value in memory. When not needed anymore, anchors should be + /// disposed. String anchors are read-only. To write strings, please see the documentation. + /// + /// Pointer path to the address of the string pointer in memory. + /// Settings to read the string. + /// An anchor for the value at the specified address. + public ValueAnchor RegisterStringAnchor(PointerPath pointerPath, + StringSettings stringSettings) + { + var memoryAdapter = new StringPointerMemoryAdapter( + new PointerPathResolver(pointerPath), stringSettings); + var anchor = new ValueAnchor(memoryAdapter, this); + _anchors.Add(anchor); + return anchor; + } + + /// Removes an anchor from the list of anchors. Designed to be called by the anchor on disposal. + /// Anchor to remove from the list of anchors. + internal void RemoveAnchor(IValueAnchor anchor) => _anchors.Remove(anchor); +} \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.cs b/src/MindControl/ProcessMemory/ProcessMemory.cs index f6b6bb4..ed7be5b 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.cs @@ -189,6 +189,10 @@ private void Detach() { if (IsAttached) { + // Dispose all anchors to prevent them from accessing memory after detaching + foreach (var anchor in _anchors) + anchor.Dispose(); + IsAttached = false; _process.Exited -= OnProcessExited; ProcessDetached?.Invoke(this, EventArgs.Empty); diff --git a/src/MindControl/Results/AllocationFailure.cs b/src/MindControl/Results/AllocationFailure.cs index 81814bb..341f73a 100644 --- a/src/MindControl/Results/AllocationFailure.cs +++ b/src/MindControl/Results/AllocationFailure.cs @@ -1,43 +1,19 @@ namespace MindControl.Results; -/// Represents a reason for a memory allocation operation to fail. -public enum AllocationFailureReason -{ - /// The target process is not attached. - DetachedProcess, - /// The arguments provided to the allocation operation are invalid. - InvalidArguments, - /// The provided limit range is not within the bounds of the target process applicative memory range. - /// - LimitRangeOutOfBounds, - /// No free memory was found in the target process that would be large enough to accomodate the specified - /// size within the searched range. - NoFreeMemoryFound -} - -/// -/// Represents a failure in a memory allocation operation. -/// -/// Reason for the failure. -public abstract record AllocationFailure(AllocationFailureReason Reason); +/// Represents a failure in a memory allocation operation. +public abstract record AllocationFailure; -/// -/// Represents a failure in a memory allocation operation when the target process is not attached. -/// -public record AllocationFailureOnDetachedProcess() - : AllocationFailure(AllocationFailureReason.DetachedProcess) +/// Represents a failure in a memory allocation operation when the target process is not attached. +public record AllocationFailureOnDetachedProcess : AllocationFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => Failure.DetachedErrorMessage; } -/// -/// Represents a failure in a memory allocation operation when the provided arguments are invalid. -/// +/// Represents a failure in a memory allocation operation when the provided arguments are invalid. /// Message that describes how the arguments fail to meet expectations. -public record AllocationFailureOnInvalidArguments(string Message) - : AllocationFailure(AllocationFailureReason.InvalidArguments) +public record AllocationFailureOnInvalidArguments(string Message) : AllocationFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -49,8 +25,7 @@ public record AllocationFailureOnInvalidArguments(string Message) /// target process applicative memory range. /// /// Applicative memory range of the target process. -public record AllocationFailureOnLimitRangeOutOfBounds(MemoryRange ApplicativeMemoryRange) - : AllocationFailure(AllocationFailureReason.LimitRangeOutOfBounds) +public record AllocationFailureOnLimitRangeOutOfBounds(MemoryRange ApplicativeMemoryRange) : AllocationFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -65,7 +40,7 @@ public override string ToString() /// Searched range in the target process. /// Last memory region address searched in the target process. public record AllocationFailureOnNoFreeMemoryFound(MemoryRange SearchedRange, UIntPtr LastRegionAddressSearched) - : AllocationFailure(AllocationFailureReason.NoFreeMemoryFound) + : AllocationFailure { /// Returns a string that represents the current object. /// A string that represents the current object. diff --git a/src/MindControl/Results/AttachFailure.cs b/src/MindControl/Results/AttachFailure.cs index 2ed9261..669776a 100644 --- a/src/MindControl/Results/AttachFailure.cs +++ b/src/MindControl/Results/AttachFailure.cs @@ -1,31 +1,10 @@ namespace MindControl.Results; -/// Represents a reason for a process attach operation to fail. -public enum AttachFailureReason -{ - /// The process with the given name or PID could not be found. - TargetProcessNotFound, - /// Multiple processes with the given name were found. - MultipleTargetProcessesFound, - /// The target process instance is not running. - TargetProcessNotRunning, - /// Attempting to attach to a 64-bit process with a 32-bit process. - IncompatibleBitness, - /// A system call involved in the process attaching operation has failed. - SystemError, -} - -/// -/// Represents a failure in a process attach operation. -/// -/// Reason for the failure. -public abstract record AttachFailure(AttachFailureReason Reason); +/// Represents a failure in a process attach operation. +public abstract record AttachFailure; -/// -/// Represents a failure in a process attach operation when the target process could not be found. -/// -public record AttachFailureOnTargetProcessNotFound() - : AttachFailure(AttachFailureReason.TargetProcessNotFound) +/// Represents a failure in a process attach operation when the target process could not be found. +public record AttachFailureOnTargetProcessNotFound : AttachFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -36,8 +15,7 @@ public record AttachFailureOnTargetProcessNotFound() /// Represents a failure in a process attach operation when multiple processes with the given name were found. /// /// Identifiers of running processes with the given name. -public record AttachFailureOnMultipleTargetProcessesFound(int[] Pids) - : AttachFailure(AttachFailureReason.MultipleTargetProcessesFound) +public record AttachFailureOnMultipleTargetProcessesFound(int[] Pids) : AttachFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -45,11 +23,8 @@ public override string ToString() => "Multiple processes with the given name were found. Use a PID to disambiguate."; } -/// -/// Represents a failure in a process attach operation when the target process is not running. -/// -public record AttachFailureOnTargetProcessNotRunning() - : AttachFailure(AttachFailureReason.TargetProcessNotRunning) +/// Represents a failure in a process attach operation when the target process is not running. +public record AttachFailureOnTargetProcessNotRunning : AttachFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -60,20 +35,16 @@ public record AttachFailureOnTargetProcessNotRunning() /// Represents a failure in a process attach operation when the target process is 64-bit and the current process is /// 32-bit. /// -public record AttachFailureOnIncompatibleBitness() - : AttachFailure(AttachFailureReason.IncompatibleBitness) +public record AttachFailureOnIncompatibleBitness : AttachFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => "Cannot open a 64-bit process from a 32-bit process."; } -/// -/// Represents a failure in a process attach operation when a system error occurs. -/// +/// Represents a failure in a process attach operation when a system error occurs. /// Details about the system error that occurred. -public record AttachFailureOnSystemError(SystemFailure Details) - : AttachFailure(AttachFailureReason.SystemError) +public record AttachFailureOnSystemError(SystemFailure Details) : AttachFailure { /// Returns a string that represents the current object. /// A string that represents the current object. diff --git a/src/MindControl/Results/FindBytesFailure.cs b/src/MindControl/Results/FindBytesFailure.cs index b7bfc57..f984e09 100644 --- a/src/MindControl/Results/FindBytesFailure.cs +++ b/src/MindControl/Results/FindBytesFailure.cs @@ -1,8 +1,6 @@ namespace MindControl.Results; -/// -/// Represents a failure occurring when the provided byte search pattern is invalid. -/// +/// Represents a failure occurring when the provided byte search pattern is invalid. /// Message that explains what makes the pattern invalid. public record InvalidBytePatternFailure(string Message) { diff --git a/src/MindControl/Results/FindStringSettingsFailure.cs b/src/MindControl/Results/FindStringSettingsFailure.cs index 6929f3e..dad671f 100644 --- a/src/MindControl/Results/FindStringSettingsFailure.cs +++ b/src/MindControl/Results/FindStringSettingsFailure.cs @@ -1,36 +1,12 @@ namespace MindControl.Results; -/// Represents a reason for a string settings search operation to fail. -public enum FindStringSettingsFailureReason -{ - /// The target process is not attached. - DetachedProcess, - /// Failure when trying to evaluate the given pointer path. - PointerPathEvaluation, - /// The target process is 32-bit, but the target memory address is not within the 32-bit address space. - /// - IncompatibleBitness, - /// Failure when trying to read the given pointer. - PointerReadFailure, - /// The given pointer is a zero pointer. - ZeroPointer, - /// Failure when trying to read bytes at the address pointed by the given pointer. - StringReadFailure, - /// No adequate settings were found to read the given string from the specified pointer. - NoSettingsFound -} - -/// -/// Represents a failure in a string settings search operation. -/// -/// Reason for the failure. -public abstract record FindStringSettingsFailure(FindStringSettingsFailureReason Reason); +/// Represents a failure in a string settings search operation. +public abstract record FindStringSettingsFailure; /// /// Represents a failure in a string settings search operation when the target process is not attached. /// -public record FindStringSettingsFailureOnDetachedProcess() - : FindStringSettingsFailure(FindStringSettingsFailureReason.DetachedProcess) +public record FindStringSettingsFailureOnDetachedProcess : FindStringSettingsFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -42,7 +18,7 @@ public record FindStringSettingsFailureOnDetachedProcess() /// /// Underlying path evaluation failure details. public record FindStringSettingsFailureOnPointerPathEvaluation(PathEvaluationFailure Details) - : FindStringSettingsFailure(FindStringSettingsFailureReason.PointerPathEvaluation) + : FindStringSettingsFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -55,8 +31,7 @@ public override string ToString() /// address is not within the 32-bit address space. /// /// Address that caused the failure. -public record FindStringSettingsFailureOnIncompatibleBitness(UIntPtr Address) - : FindStringSettingsFailure(FindStringSettingsFailureReason.IncompatibleBitness) +public record FindStringSettingsFailureOnIncompatibleBitness(UIntPtr Address) : FindStringSettingsFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -68,8 +43,7 @@ public override string ToString() /// Represents a failure in a string settings search operation when failing to read the value of the given pointer. /// /// Underlying read failure details. -public record FindStringSettingsFailureOnPointerReadFailure(ReadFailure Details) - : FindStringSettingsFailure(FindStringSettingsFailureReason.PointerReadFailure) +public record FindStringSettingsFailureOnPointerReadFailure(ReadFailure Details) : FindStringSettingsFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -80,8 +54,7 @@ public override string ToString() /// /// Represents a failure in a string settings search operation when the given pointer is a zero pointer. /// -public record FindStringSettingsFailureOnZeroPointer() - : FindStringSettingsFailure(FindStringSettingsFailureReason.ZeroPointer) +public record FindStringSettingsFailureOnZeroPointer : FindStringSettingsFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -93,8 +66,7 @@ public record FindStringSettingsFailureOnZeroPointer() /// given pointer. /// /// Underlying read failure details. -public record FindStringSettingsFailureOnStringReadFailure(ReadFailure Details) - : FindStringSettingsFailure(FindStringSettingsFailureReason.StringReadFailure) +public record FindStringSettingsFailureOnStringReadFailure(ReadFailure Details) : FindStringSettingsFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -106,8 +78,7 @@ public override string ToString() /// Represents a failure in a string settings search operation when no adequate settings were found to read the given /// string from the specified pointer. /// -public record FindStringSettingsFailureOnNoSettingsFound() - : FindStringSettingsFailure(FindStringSettingsFailureReason.NoSettingsFound) +public record FindStringSettingsFailureOnNoSettingsFound : FindStringSettingsFailure { /// Returns a string that represents the current object. /// A string that represents the current object. diff --git a/src/MindControl/Results/InjectionFailure.cs b/src/MindControl/Results/InjectionFailure.cs index 275971a..26edac6 100644 --- a/src/MindControl/Results/InjectionFailure.cs +++ b/src/MindControl/Results/InjectionFailure.cs @@ -1,32 +1,10 @@ namespace MindControl.Results; -/// Represents a reason for an injection operation to fail. -public enum InjectionFailureReason -{ - /// The target process is not attached. - DetachedProcess, - /// The library file to inject was not found. - LibraryFileNotFound, - /// The module to inject is already loaded in the target process. - ModuleAlreadyLoaded, - /// Failure when trying to store function parameters. - ParameterStorageFailure, - /// Failure when running the library loading thread. - ThreadFailure, - /// The library failed to load. - LoadLibraryFailure -} +/// Represents a failure in a library injection operation. +public abstract record InjectionFailure; -/// -/// Represents a failure in an injection operation. -/// -public abstract record InjectionFailure(InjectionFailureReason Reason); - -/// -/// Represents a failure in an injection operation when the target process is not attached. -/// -public record InjectionFailureOnDetachedProcess() - : InjectionFailure(InjectionFailureReason.DetachedProcess) +/// Represents a failure in a library injection operation when the target process is not attached. +public record InjectionFailureOnDetachedProcess : InjectionFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -34,11 +12,10 @@ public record InjectionFailureOnDetachedProcess() } /// -/// Represents a failure in an injection operation when the library file to inject was not found. +/// Represents a failure in a library injection operation when the library file to inject was not found. /// /// Path to the library file that was not found. -public record InjectionFailureOnLibraryFileNotFound(string LibraryPath) - : InjectionFailure(InjectionFailureReason.LibraryFileNotFound) +public record InjectionFailureOnLibraryFileNotFound(string LibraryPath) : InjectionFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -47,10 +24,10 @@ public override string ToString() } /// -/// Represents a failure in an injection operation when the module to inject is already loaded in the target process. +/// Represents a failure in a library injection operation when the module to inject is already loaded in the target +/// process. /// -public record InjectionFailureOnModuleAlreadyLoaded() - : InjectionFailure(InjectionFailureReason.ModuleAlreadyLoaded) +public record InjectionFailureOnModuleAlreadyLoaded : InjectionFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -58,12 +35,9 @@ public override string ToString() => "The module to inject is already loaded in the target process."; } -/// -/// Represents a failure in an injection operation when trying to store function parameters. -/// +/// Represents a failure in a library injection operation when trying to store function parameters. /// Details about the failure. -public record InjectionFailureOnParameterStorage(StoreFailure Details) - : InjectionFailure(InjectionFailureReason.ParameterStorageFailure) +public record InjectionFailureOnParameterStorage(StoreFailure Details) : InjectionFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -71,12 +45,9 @@ public override string ToString() => $"Failed to store function parameters required to inject the library: {Details}"; } -/// -/// Represents a failure in an injection operation when running the library loading thread. -/// +/// Represents a failure in a library injection operation when running the library loading thread. /// Details about the failure. -public record InjectionFailureOnThreadFailure(ThreadFailure Details) - : InjectionFailure(InjectionFailureReason.ThreadFailure) +public record InjectionFailureOnThreadFailure(ThreadFailure Details) : InjectionFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -84,14 +55,11 @@ public override string ToString() => $"Failed to inject the library due to a remote thread failure: {Details}"; } -/// -/// Represents a failure in an injection operation when the library function call fails. -/// -public record InjectionFailureOnLoadLibraryFailure() - : InjectionFailure(InjectionFailureReason.LoadLibraryFailure) +/// Represents a failure in a library injection operation when the library function call fails. +public record InjectionFailureOnLoadLibraryFailure : InjectionFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => "The LoadLibraryW function returned a status code of 0, indicating failure. Check that the library is valid and compatible with the target process (32-bit vs 64-bit)."; + => "The LoadLibraryW function returned an exit code of 0, indicating failure. Check that the library is valid and compatible with the target process (32-bit vs 64-bit)."; } diff --git a/src/MindControl/Results/NotSupportedFailure.cs b/src/MindControl/Results/NotSupportedFailure.cs new file mode 100644 index 0000000..979e17f --- /dev/null +++ b/src/MindControl/Results/NotSupportedFailure.cs @@ -0,0 +1,13 @@ +namespace MindControl.Results; + +/// Represents a failure when an operation is not supported. +/// Message that describes why the operation is not supported. +public record NotSupportedFailure(string Message) +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + { + return $"This operation is not supported: {Message}"; + } +} \ No newline at end of file diff --git a/src/MindControl/Results/PathEvaluationFailure.cs b/src/MindControl/Results/PathEvaluationFailure.cs index 90106e2..ec4d958 100644 --- a/src/MindControl/Results/PathEvaluationFailure.cs +++ b/src/MindControl/Results/PathEvaluationFailure.cs @@ -1,31 +1,10 @@ namespace MindControl.Results; -/// Represents a reason for a path evaluation operation to fail. -public enum PathEvaluationFailureReason -{ - /// The target process is not attached. - DetachedProcess, - /// The target process is 32-bit, but the path is not compatible with a 32-bit address space. - IncompatibleBitness, - /// The module specified in the pointer path was not found. - BaseModuleNotFound, - /// A pointer in the path is a zero pointer or otherwise out of the target process address space. - PointerOutOfRange, - /// Failure when attempting to read a pointer from the path. - PointerReadFailure -} +/// Represents a failure in a path evaluation operation. +public abstract record PathEvaluationFailure; -/// -/// Represents a failure in a path evaluation operation. -/// -/// Reason for the failure. -public abstract record PathEvaluationFailure(PathEvaluationFailureReason Reason); - -/// -/// Represents a failure in a path evaluation operation when the target process is not attached. -/// -public record PathEvaluationFailureOnDetachedProcess() - : PathEvaluationFailure(PathEvaluationFailureReason.DetachedProcess) +/// Represents a failure in a path evaluation operation when the target process is not attached. +public record PathEvaluationFailureOnDetachedProcess : PathEvaluationFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -38,8 +17,7 @@ public record PathEvaluationFailureOnDetachedProcess() /// /// Address where the value causing the issue was read. May be null if the first address /// in the path caused the failure. -public record PathEvaluationFailureOnIncompatibleBitness(UIntPtr? PreviousAddress = null) - : PathEvaluationFailure(PathEvaluationFailureReason.IncompatibleBitness) +public record PathEvaluationFailureOnIncompatibleBitness(UIntPtr? PreviousAddress = null) : PathEvaluationFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -52,8 +30,7 @@ public override string ToString() /// found. /// /// Name of the module that was not found. -public record PathEvaluationFailureOnBaseModuleNotFound(string ModuleName) - : PathEvaluationFailure(PathEvaluationFailureReason.BaseModuleNotFound) +public record PathEvaluationFailureOnBaseModuleNotFound(string ModuleName) : PathEvaluationFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -69,7 +46,7 @@ public override string ToString() /// in the path caused the failure. /// Offset that caused the failure. public record PathEvaluationFailureOnPointerOutOfRange(UIntPtr? PreviousAddress, PointerOffset Offset) - : PathEvaluationFailure(PathEvaluationFailureReason.PointerOutOfRange) + : PathEvaluationFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -82,8 +59,7 @@ public override string ToString() /// /// Address that caused the failure. /// Details about the failure. -public record PathEvaluationFailureOnPointerReadFailure(UIntPtr Address, ReadFailure Details) - : PathEvaluationFailure(PathEvaluationFailureReason.PointerReadFailure) +public record PathEvaluationFailureOnPointerReadFailure(UIntPtr Address, ReadFailure Details) : PathEvaluationFailure { /// Returns a string that represents the current object. /// A string that represents the current object. diff --git a/src/MindControl/Results/ReadFailure.cs b/src/MindControl/Results/ReadFailure.cs index 1dca9dc..d4e4482 100644 --- a/src/MindControl/Results/ReadFailure.cs +++ b/src/MindControl/Results/ReadFailure.cs @@ -1,67 +1,44 @@ namespace MindControl.Results; -/// Represents a reason for a memory read operation to fail. -public enum ReadFailureReason -{ - /// The target process is not attached. - DetachedProcess, - /// The arguments provided to the memory read operation are invalid. - InvalidArguments, - /// Failure when evaluating the pointer path to the target address. - PointerPathEvaluationFailure, - /// The target process is 32-bit, but the target memory address is not within the 32-bit address space. - /// - IncompatibleBitness, - /// The target pointer is a zero pointer. - ZeroPointer, - /// Failure when invoking the system API to read the target memory. - SystemReadFailure, - /// Failure when trying to convert the bytes read from memory to the target type. - ConversionFailure, - /// The type to read is not supported. - UnsupportedType -} - -/// -/// Represents a failure in a memory read operation. -/// -/// Reason for the failure. -public abstract record ReadFailure(ReadFailureReason Reason); +/// Represents a failure in a memory read operation. +public abstract record ReadFailure; -/// -/// Represents a failure in a memory read operation when the target process is not attached. -/// -public record ReadFailureOnDetachedProcess() - : ReadFailure(ReadFailureReason.DetachedProcess) +/// Represents a failure in a memory read operation when the target process is not attached. +public record ReadFailureOnDetachedProcess : ReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => Failure.DetachedErrorMessage; } -/// -/// Represents a failure in a memory read operation when the arguments provided are invalid. -/// +/// Represents a failure in a memory read operation when the arguments provided are invalid. /// Message that describes how the arguments fail to meet expectations. -public record ReadFailureOnInvalidArguments(string Message) - : ReadFailure(ReadFailureReason.InvalidArguments) +public record ReadFailureOnInvalidArguments(string Message) : ReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => $"The arguments provided are invalid: {Message}"; } +/// Represents a failure in a memory read operation when resolving the address in the target process. +/// Details about the failure. +/// Type of the underlying failure. +public record ReadFailureOnAddressResolution(T Details) : ReadFailure +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failed to resolve the address: {Details}"; +} + /// /// Represents a failure in a memory read operation when evaluating the pointer path to the target memory. /// /// Details about the failure. -public record ReadFailureOnPointerPathEvaluation(PathEvaluationFailure Details) - : ReadFailure(ReadFailureReason.PointerPathEvaluationFailure) +public record ReadFailureOnPointerPathEvaluation(PathEvaluationFailure Details) : ReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() - => $"Failed to evaluate the specified pointer path: {Details}"; + public override string ToString() => $"Failed to evaluate the specified pointer path: {Details}"; } /// @@ -69,8 +46,7 @@ public override string ToString() /// not within the 32-bit address space. /// /// Address that caused the failure. -public record ReadFailureOnIncompatibleBitness(UIntPtr Address) - : ReadFailure(ReadFailureReason.IncompatibleBitness) +public record ReadFailureOnIncompatibleBitness(UIntPtr Address) : ReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -78,24 +54,19 @@ public override string ToString() => $"The address to read, {Address}, is a 64-bit address, but the target process is 32-bit."; } -/// -/// Represents a failure in a memory read operation when the target pointer is a zero pointer. -/// -public record ReadFailureOnZeroPointer() - : ReadFailure(ReadFailureReason.ZeroPointer) +/// Represents a failure in a memory read operation when the target pointer is a zero pointer. +public record ReadFailureOnZeroPointer : ReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() - => "The address to read is a zero pointer."; + public override string ToString() => "The address to read is a zero pointer."; } /// /// Represents a failure in a memory read operation when invoking the system API to read the target memory. /// /// Details about the failure. -public record ReadFailureOnSystemRead(SystemFailure Details) - : ReadFailure(ReadFailureReason.SystemReadFailure) +public record ReadFailureOnSystemRead(SystemFailure Details) : ReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -106,22 +77,20 @@ public record ReadFailureOnSystemRead(SystemFailure Details) /// Represents a failure in a memory read operation when trying to convert the bytes read from memory to the target /// type. /// -public record ReadFailureOnConversionFailure() - : ReadFailure(ReadFailureReason.ConversionFailure) +public record ReadFailureOnConversionFailure : ReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() => "Failed to convert the bytes read from memory to the target type. Check that the type does not contain references or pointers."; + public override string ToString() + => "Failed to convert the bytes read from memory to the target type. Check that the type does not contain references or pointers."; } -/// -/// Represents a failure in a memory read operation when the type to read is not supported. -/// +/// Represents a failure in a memory read operation when the type to read is not supported. /// Type that caused the failure. -public record ReadFailureOnUnsupportedType(Type ProvidedType) - : ReadFailure(ReadFailureReason.UnsupportedType) +public record ReadFailureOnUnsupportedType(Type ProvidedType) : ReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() => $"The type {ProvidedType} is not supported. Reference types are not supported."; + public override string ToString() + => $"The type {ProvidedType} is not supported. Reference types are not supported."; } \ No newline at end of file diff --git a/src/MindControl/Results/ReservationFailure.cs b/src/MindControl/Results/ReservationFailure.cs index 1a10735..42a32f9 100644 --- a/src/MindControl/Results/ReservationFailure.cs +++ b/src/MindControl/Results/ReservationFailure.cs @@ -1,27 +1,12 @@ namespace MindControl.Results; -/// Represents a reason for a memory reservation operation to fail. -public enum ReservationFailureReason -{ - /// The target allocation has been disposed. - DisposedAllocation, - /// The arguments provided to the reservation operation are invalid. - InvalidArguments, - /// No space is available within the allocated memory range to reserve the specified size. - NoSpaceAvailable -} - -/// -/// Represents a failure in a memory reservation operation. -/// -/// Reason for the failure. -public abstract record ReservationFailure(ReservationFailureReason Reason); +/// Represents a failure in a memory reservation operation. +public abstract record ReservationFailure; /// /// Represents a failure in a memory reservation operation when the target allocation has been disposed. /// -public record ReservationFailureOnDisposedAllocation() - : ReservationFailure(ReservationFailureReason.DisposedAllocation) +public record ReservationFailureOnDisposedAllocation : ReservationFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -32,8 +17,7 @@ public record ReservationFailureOnDisposedAllocation() /// Represents a failure in a memory reservation operation when the provided arguments are invalid. /// /// Message that describes how the arguments fail to meet expectations. -public record ReservationFailureOnInvalidArguments(string Message) - : ReservationFailure(ReservationFailureReason.InvalidArguments) +public record ReservationFailureOnInvalidArguments(string Message) : ReservationFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -44,8 +28,7 @@ public record ReservationFailureOnInvalidArguments(string Message) /// Represents a failure in a memory reservation operation when no space is available within the allocated memory range /// to reserve the specified size. /// -public record ReservationFailureOnNoSpaceAvailable() - : ReservationFailure(ReservationFailureReason.NoSpaceAvailable) +public record ReservationFailureOnNoSpaceAvailable : ReservationFailure { /// Returns a string that represents the current object. /// A string that represents the current object. diff --git a/src/MindControl/Results/StoreFailure.cs b/src/MindControl/Results/StoreFailure.cs index 9a4a40f..af10747 100644 --- a/src/MindControl/Results/StoreFailure.cs +++ b/src/MindControl/Results/StoreFailure.cs @@ -1,27 +1,10 @@ namespace MindControl.Results; -/// Represents a reason for a store operation to fail. -public enum StoreFailureReason -{ - /// The target process is not attached. - DetachedProcess, - /// The arguments provided to the store operation are invalid. - InvalidArguments, - /// The allocation operation failed. - AllocationFailure, - /// The reservation operation failed. - ReservationFailure, - /// The write operation failed. - WriteFailure -} - /// Represents a failure in a memory store operation. -/// Reason for the failure. -public abstract record StoreFailure(StoreFailureReason Reason); +public abstract record StoreFailure; /// Represents a failure in a memory store operation when the target process is not attached. -public record StoreFailureOnDetachedProcess() - : StoreFailure(StoreFailureReason.DetachedProcess) +public record StoreFailureOnDetachedProcess : StoreFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -30,8 +13,7 @@ public record StoreFailureOnDetachedProcess() /// Represents a failure in a memory store operation when the provided arguments are invalid. /// Message that describes how the arguments fail to meet expectations. -public record StoreFailureOnInvalidArguments(string Message) - : StoreFailure(StoreFailureReason.InvalidArguments) +public record StoreFailureOnInvalidArguments(string Message) : StoreFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -40,8 +22,7 @@ public record StoreFailureOnInvalidArguments(string Message) /// Represents a failure in a memory store operation when the allocation operation failed. /// The allocation failure that caused the store operation to fail. -public record StoreFailureOnAllocation(AllocationFailure Details) - : StoreFailure(StoreFailureReason.AllocationFailure) +public record StoreFailureOnAllocation(AllocationFailure Details) : StoreFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -50,8 +31,7 @@ public record StoreFailureOnAllocation(AllocationFailure Details) /// Represents a failure in a memory store operation when the reservation operation failed. /// The reservation failure that caused the store operation to fail. -public record StoreFailureOnReservation(ReservationFailure Details) - : StoreFailure(StoreFailureReason.ReservationFailure) +public record StoreFailureOnReservation(ReservationFailure Details) : StoreFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -60,8 +40,7 @@ public record StoreFailureOnReservation(ReservationFailure Details) /// Represents a failure in a memory store operation when the write operation failed. /// The write failure that caused the store operation to fail. -public record StoreFailureOnWrite(WriteFailure Details) - : StoreFailure(StoreFailureReason.WriteFailure) +public record StoreFailureOnWrite(WriteFailure Details) : StoreFailure { /// Returns a string that represents the current object. /// A string that represents the current object. diff --git a/src/MindControl/Results/StringReadFailure.cs b/src/MindControl/Results/StringReadFailure.cs index 51b132a..c410837 100644 --- a/src/MindControl/Results/StringReadFailure.cs +++ b/src/MindControl/Results/StringReadFailure.cs @@ -1,62 +1,37 @@ namespace MindControl.Results; -/// Represents a reason for a string read operation to fail. -public enum StringReadFailureReason -{ - /// The target process is not attached. - DetachedProcess, - /// Failure when evaluating the pointer path to the target address. - PointerPathEvaluationFailure, - /// The string read operation failed because the settings provided are invalid. - InvalidSettings, - /// The string read operation failed because the target process is 32-bit, but the target memory address is - /// not within the 32-bit address space. - IncompatibleBitness, - /// The string read operation failed because the target pointer is a zero pointer. - ZeroPointer, - /// The pointer read operation failed. - PointerReadFailure, - /// A read operation failed when attempting to read bytes from the actual string. - StringBytesReadFailure, - /// The length prefix of the string was evaluated to a value exceeding the configured max length, or a null - /// terminator was not found within the configured max length. - StringTooLong -} - -/// -/// Represents a failure in a string read operation. -/// -/// Reason for the failure. -public abstract record StringReadFailure(StringReadFailureReason Reason); +/// Represents a failure in a string read operation. +public abstract record StringReadFailure; -/// -/// Represents a failure in a string read operation when the target process is not attached. -/// -public record StringReadFailureOnDetachedProcess() - : StringReadFailure(StringReadFailureReason.DetachedProcess) +/// Represents a failure in a string read operation when the target process is not attached. +public record StringReadFailureOnDetachedProcess : StringReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => Failure.DetachedErrorMessage; } -/// -/// Represents a failure in a string read operation when the pointer path evaluation failed. -/// +/// Represents a failure in a string read operation when the pointer path evaluation failed. /// Details about the path evaluation failure. -public record StringReadFailureOnPointerPathEvaluation(PathEvaluationFailure Details) - : StringReadFailure(StringReadFailureReason.PointerPathEvaluationFailure) +public record StringReadFailureOnPointerPathEvaluation(PathEvaluationFailure Details) : StringReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => $"Failed to evaluate the specified pointer path: {Details}"; } -/// -/// Represents a failure in a string read operation when the settings provided are invalid. -/// -public record StringReadFailureOnInvalidSettings() - : StringReadFailure(StringReadFailureReason.InvalidSettings) +/// Represents a failure in a string read operation when resolving the address in the target process. +/// Details about the address resolution failure. +/// Type of the underlying failure. +public record StringReadFailureOnAddressResolution(T Details) : StringReadFailure +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failed to resolve the address: {Details}"; +} + +/// Represents a failure in a string read operation when the settings provided are invalid. +public record StringReadFailureOnInvalidSettings : StringReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -69,44 +44,34 @@ public override string ToString() /// not within the 32-bit address space. /// /// Address that caused the failure. -public record StringReadFailureOnIncompatibleBitness(UIntPtr Address) - : StringReadFailure(StringReadFailureReason.IncompatibleBitness) +public record StringReadFailureOnIncompatibleBitness(UIntPtr Address) : StringReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => $"The target address {Address} is not within the 32-bit address space."; } -/// -/// Represents a failure in a string read operation when the target pointer is a zero pointer. -/// -public record StringReadFailureOnZeroPointer() - : StringReadFailure(StringReadFailureReason.ZeroPointer) +/// Represents a failure in a string read operation when the target pointer is a zero pointer. +public record StringReadFailureOnZeroPointer : StringReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => "The target address is a zero pointer."; } -/// -/// Represents a failure in a string read operation when the pointer read operation failed. -/// +/// Represents a failure in a string read operation when the pointer read operation failed. /// Details about the pointer read failure. -public record StringReadFailureOnPointerReadFailure(ReadFailure Details) - : StringReadFailure(StringReadFailureReason.PointerReadFailure) +public record StringReadFailureOnPointerReadFailure(ReadFailure Details) : StringReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => $"The pointer read operation failed: {Details}"; } -/// -/// Represents a failure in a string read operation when a read operation on the string bytes failed. -/// +/// Represents a failure in a string read operation when a read operation on the string bytes failed. /// Address where the string read operation failed. /// Details about the read failure. -public record StringReadFailureOnStringBytesReadFailure(UIntPtr Address, ReadFailure Details) - : StringReadFailure(StringReadFailureReason.StringBytesReadFailure) +public record StringReadFailureOnStringBytesReadFailure(UIntPtr Address, ReadFailure Details) : StringReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -118,8 +83,7 @@ public record StringReadFailureOnStringBytesReadFailure(UIntPtr Address, ReadFai /// configured max length, or a null terminator was not found within the configured max length. /// /// Length read from the length prefix bytes, in case a length prefix was set. -public record StringReadFailureOnStringTooLong(ulong? LengthPrefixValue) - : StringReadFailure(StringReadFailureReason.StringTooLong) +public record StringReadFailureOnStringTooLong(ulong? LengthPrefixValue) : StringReadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. diff --git a/src/MindControl/Results/SystemFailure.cs b/src/MindControl/Results/SystemFailure.cs index ceab3ba..9bcd55d 100644 --- a/src/MindControl/Results/SystemFailure.cs +++ b/src/MindControl/Results/SystemFailure.cs @@ -1,34 +1,12 @@ namespace MindControl.Results; -/// -/// Represents a reason for a system operation to fail. -/// -public enum SystemFailureReason -{ - /// - /// The arguments provided to the system operation are invalid. - /// - ArgumentsInvalid, - - /// - /// The system API call failed. - /// - OperatingSystemCallFailed -} - -/// -/// Represents a failure in an operating system operation. -/// -/// Reason for the failure. -public abstract record SystemFailure(SystemFailureReason Reason); +/// Represents a failure in an operating system operation. +public abstract record SystemFailure; -/// -/// Represents a failure in an operating system operation when the provided arguments are invalid. -/// +/// Represents a failure in an operating system operation when the provided arguments are invalid. /// Name of the argument that caused the failure. /// Message that describes how the argument fails to meet expectations. -public record SystemFailureOnInvalidArgument(string ArgumentName, string Message) - : SystemFailure(SystemFailureReason.ArgumentsInvalid) +public record SystemFailureOnInvalidArgument(string ArgumentName, string Message) : SystemFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -36,13 +14,10 @@ public override string ToString() => $"The value provided for \"{ArgumentName}\" is invalid: {Message}"; } -/// -/// Represents a failure in a system API call. -/// +/// Represents a failure in a system API call. /// Numeric code that identifies the error. Typically provided by the operating system. /// Message that describes the error. Typically provided by the operating system. -public record OperatingSystemCallFailure(int ErrorCode, string ErrorMessage) - : SystemFailure(SystemFailureReason.OperatingSystemCallFailed) +public record OperatingSystemCallFailure(int ErrorCode, string ErrorMessage) : SystemFailure { /// Returns a string that represents the current object. /// A string that represents the current object. diff --git a/src/MindControl/Results/ThreadFailure.cs b/src/MindControl/Results/ThreadFailure.cs index a4847fb..d9ddf67 100644 --- a/src/MindControl/Results/ThreadFailure.cs +++ b/src/MindControl/Results/ThreadFailure.cs @@ -1,84 +1,46 @@ namespace MindControl.Results; -/// Represents a reason for a thread operation to fail. -public enum ThreadFailureReason -{ - /// The target process is not attached. - DetachedProcess, - /// Invalid arguments were provided to the thread operation. - InvalidArguments, - /// The thread handle has already been disposed. - DisposedInstance, - /// Failure when evaluating the pointer path to the target address. - PointerPathEvaluationFailure, - /// The target function cannot be found. - FunctionNotFound, - /// Failure when waiting for a thread to finish execution for too long. - ThreadWaitTimeout, - /// Failure when a waiting operation was abandoned. - WaitAbandoned, - /// Failure when calling a system API function. - SystemFailure -} - -/// -/// Represents a failure in a thread operation. -/// -public record ThreadFailure(ThreadFailureReason Reason); +/// Represents a failure in a thread operation. +public record ThreadFailure; -/// -/// Represents a failure in a thread operation when the target process is not attached. -/// -public record ThreadFailureOnDetachedProcess() - : ThreadFailure(ThreadFailureReason.DetachedProcess) +/// Represents a failure in a thread operation when the target process is not attached. +public record ThreadFailureOnDetachedProcess : ThreadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => Failure.DetachedErrorMessage; } -/// -/// Represents a failure in a thread operation when the arguments provided are invalid. -/// +/// Represents a failure in a thread operation when the arguments provided are invalid. /// Message that describes how the arguments fail to meet expectations. -public record ThreadFailureOnInvalidArguments(string Message) - : ThreadFailure(ThreadFailureReason.InvalidArguments) +public record ThreadFailureOnInvalidArguments(string Message) : ThreadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() - => $"The arguments provided are invalid: {Message}"; + public override string ToString() => $"The arguments provided are invalid: {Message}"; } -/// -/// Represents a failure in a thread operation when the thread handle has already been disposed. -/// -public record ThreadFailureOnDisposedInstance() - : ThreadFailure(ThreadFailureReason.DisposedInstance) +/// Represents a failure in a thread operation when the thread handle has already been disposed. +public record ThreadFailureOnDisposedInstance : ThreadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => "The thread handle has already been disposed."; } -/// -/// Represents a failure in a thread operation when evaluating the pointer path to the target address. +/// Represents a failure in a thread operation when evaluating the pointer path to the target address. /// /// Details about the failure. -public record ThreadFailureOnPointerPathEvaluation(PathEvaluationFailure Details) - : ThreadFailure(ThreadFailureReason.PointerPathEvaluationFailure) +public record ThreadFailureOnPointerPathEvaluation(PathEvaluationFailure Details) : ThreadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() => $"Failure when evaluating the pointer path to the target address: {Details}"; } -/// -/// Represents a failure in a thread operation when the target function cannot be found. -/// +/// Represents a failure in a thread operation when the target function cannot be found. /// Message including details about the failure. -public record ThreadFailureOnFunctionNotFound(string Message) - : ThreadFailure(ThreadFailureReason.FunctionNotFound) +public record ThreadFailureOnFunctionNotFound(string Message) : ThreadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -88,8 +50,7 @@ public record ThreadFailureOnFunctionNotFound(string Message) /// /// Represents a failure in a thread operation when the thread did not finish execution within the specified timeout. /// -public record ThreadFailureOnWaitTimeout() - : ThreadFailure(ThreadFailureReason.ThreadWaitTimeout) +public record ThreadFailureOnWaitTimeout : ThreadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -97,11 +58,8 @@ public override string ToString() => "The thread did not finish execution within the specified timeout."; } -/// -/// Represents a failure in a thread operation when a waiting operation was abandoned. -/// -public record ThreadFailureOnWaitAbandoned() - : ThreadFailure(ThreadFailureReason.WaitAbandoned) +/// Represents a failure in a thread operation when a waiting operation was abandoned. +public record ThreadFailureOnWaitAbandoned : ThreadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -113,11 +71,9 @@ public record ThreadFailureOnWaitAbandoned() /// /// Message that details what operation failed. /// Details about the failure. -public record ThreadFailureOnSystemFailure(string Message, SystemFailure Details) - : ThreadFailure(ThreadFailureReason.SystemFailure) +public record ThreadFailureOnSystemFailure(string Message, SystemFailure Details) : ThreadFailure { /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() - => $"A system API function call failed: {Message} / {Details}"; + public override string ToString() => $"A system API function call failed: {Message} / {Details}"; } \ No newline at end of file diff --git a/src/MindControl/Results/WriteFailure.cs b/src/MindControl/Results/WriteFailure.cs index 2d872eb..dcd0d54 100644 --- a/src/MindControl/Results/WriteFailure.cs +++ b/src/MindControl/Results/WriteFailure.cs @@ -1,46 +1,10 @@ namespace MindControl.Results; -/// -/// Represents a reason for a memory write operation to fail. -/// -public enum WriteFailureReason -{ - /// The target process is not attached. - DetachedProcess, - /// Failure when evaluating the pointer path to the target memory. - PointerPathEvaluationFailure, - /// The arguments provided to the memory read operation are invalid. - InvalidArguments, - /// The type to write is not supported. - UnsupportedType, - /// The target process is 32-bit, but the target memory address is not within the 32-bit address space. - /// - IncompatibleBitness, - /// The target address is a zero pointer. - ZeroPointer, - /// Failure when invoking the system API to remove the protection properties of the target memory space. - /// - SystemProtectionRemovalFailure, - /// Failure when invoking the system API to restore the protection properties of the target memory space - /// after writing. - SystemProtectionRestorationFailure, - /// Failure when invoking the system API to write bytes in memory. - SystemWriteFailure, - /// Failure when trying to convert the value to write to an array of bytes to write in memory. - ConversionFailure -} - -/// -/// Represents a failure in a memory write operation. -/// -/// Reason for the failure. -public abstract record WriteFailure(WriteFailureReason Reason); +/// Represents a failure in a memory write operation. +public abstract record WriteFailure; -/// -/// Represents a failure in a memory write operation when the target process is not attached. -/// -public record WriteFailureOnDetachedProcess() - : WriteFailure(WriteFailureReason.DetachedProcess) +/// Represents a failure in a memory write operation when the target process is not attached. +public record WriteFailureOnDetachedProcess : WriteFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -51,21 +15,28 @@ public record WriteFailureOnDetachedProcess() /// Represents a failure in a memory write operation when evaluating the pointer path to the target memory. /// /// Details about the failure. -public record WriteFailureOnPointerPathEvaluation(PathEvaluationFailure Details) - : WriteFailure(WriteFailureReason.PointerPathEvaluationFailure) +public record WriteFailureOnPointerPathEvaluation(PathEvaluationFailure Details) : WriteFailure { /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() - => $"Failed to evaluate the specified pointer path: {Details}"; + public override string ToString() => $"Failed to evaluate the specified pointer path: {Details}"; } /// -/// Represents a failure in a memory write operation when the arguments provided are invalid. +/// Represents a failure in a memory write operation when resolving the address in the target process. /// +/// Details about the failure. +/// Type of the underlying failure. +public record WriteFailureOnAddressResolution(T Details) : WriteFailure +{ + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => $"Failed to resolve the address: {Details}"; +} + +/// Represents a failure in a memory write operation when the arguments provided are invalid. /// Message that describes how the arguments fail to meet expectations. -public record WriteFailureOnInvalidArguments(string Message) - : WriteFailure(WriteFailureReason.InvalidArguments) +public record WriteFailureOnInvalidArguments(string Message) : WriteFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -76,8 +47,7 @@ public record WriteFailureOnInvalidArguments(string Message) /// Represents a failure in a memory write operation when the value to write cannot be converted to an array of bytes. /// /// Type that caused the failure. -public record WriteFailureOnUnsupportedType(Type Type) - : WriteFailure(WriteFailureReason.UnsupportedType) +public record WriteFailureOnUnsupportedType(Type Type) : WriteFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -89,8 +59,7 @@ public record WriteFailureOnUnsupportedType(Type Type) /// is not within the 32-bit address space. /// /// Address that caused the failure. -public record WriteFailureOnIncompatibleBitness(UIntPtr Address) - : WriteFailure(WriteFailureReason.IncompatibleBitness) +public record WriteFailureOnIncompatibleBitness(UIntPtr Address) : WriteFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -98,11 +67,8 @@ public override string ToString() => $"The pointer to write, {Address}, is too large for a 32-bit process. If you want to write an 8-byte value and not a memory address, use a ulong instead."; } -/// -/// Represents a failure in a memory write operation when the address to write is a zero pointer. -/// -public record WriteFailureOnZeroPointer() - : WriteFailure(WriteFailureReason.ZeroPointer) +/// Represents a failure in a memory write operation when the address to write is a zero pointer. +public record WriteFailureOnZeroPointer : WriteFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -115,8 +81,7 @@ public record WriteFailureOnZeroPointer() /// /// Address where the operation failed. /// Details about the failure. -public record WriteFailureOnSystemProtectionRemoval(UIntPtr Address, SystemFailure Details) - : WriteFailure(WriteFailureReason.SystemProtectionRemovalFailure) +public record WriteFailureOnSystemProtectionRemoval(UIntPtr Address, SystemFailure Details) : WriteFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -130,8 +95,7 @@ public override string ToString() /// /// Address where the operation failed. /// Details about the failure. -public record WriteFailureOnSystemProtectionRestoration(UIntPtr Address, SystemFailure Details) - : WriteFailure(WriteFailureReason.SystemProtectionRestorationFailure) +public record WriteFailureOnSystemProtectionRestoration(UIntPtr Address, SystemFailure Details) : WriteFailure { /// Returns a string that represents the current object. /// A string that represents the current object. @@ -144,13 +108,11 @@ public override string ToString() /// /// Address where the write operation failed. /// Details about the failure. -public record WriteFailureOnSystemWrite(UIntPtr Address, SystemFailure Details) - : WriteFailure(WriteFailureReason.SystemWriteFailure) +public record WriteFailureOnSystemWrite(UIntPtr Address, SystemFailure Details) : WriteFailure { /// Returns a string that represents the current object. /// A string that represents the current object. - public override string ToString() - => $"Failed to write at the address {Address}: {Details}"; + public override string ToString() => $"Failed to write at the address {Address}: {Details}"; } /// @@ -159,8 +121,7 @@ public override string ToString() /// /// Type that caused the failure. /// Exception that occurred during the conversion. -public record WriteFailureOnConversion(Type Type, Exception ConversionException) - : WriteFailure(WriteFailureReason.ConversionFailure) +public record WriteFailureOnConversion(Type Type, Exception ConversionException) : WriteFailure { /// Returns a string that represents the current object. /// A string that represents the current object. From 377c942376565185ddaecd31f0aedfaba66ca0a2 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 13 Jul 2024 17:40:57 +0200 Subject: [PATCH 43/66] Tests for anchor Read/Write --- .../Anchors/GenericMemoryAdapter.cs | 2 +- .../ProcessMemory/ProcessMemory.Anchors.cs | 6 +- .../ProcessMemoryAnchorTest.cs | 191 ++++++++++++++++++ 3 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs diff --git a/src/MindControl/Anchors/GenericMemoryAdapter.cs b/src/MindControl/Anchors/GenericMemoryAdapter.cs index a18d021..4aa9d88 100644 --- a/src/MindControl/Anchors/GenericMemoryAdapter.cs +++ b/src/MindControl/Anchors/GenericMemoryAdapter.cs @@ -35,6 +35,6 @@ public Result Write(ProcessMemory processMemory, TValue value) if (addressResult.IsFailure) return new WriteFailureOnAddressResolution(addressResult.Error); - return processMemory.Write(addressResult.Value, value); + return processMemory.Write(addressResult.Value, value, MemoryProtectionStrategy.Ignore); } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs b/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs index 8476a5c..48fedb1 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs @@ -81,7 +81,7 @@ public ValueAnchor RegisterByteArrayAnchor(Po /// Address of the string pointer in memory. /// Settings to read the string. /// An anchor for the value at the specified address. - public ValueAnchor RegisterStringAnchor(UIntPtr address, + public ValueAnchor RegisterStringPointerAnchor(UIntPtr address, StringSettings stringSettings) { var memoryAdapter = new StringPointerMemoryAdapter(new LiteralAddressResolver(address), stringSettings); @@ -98,8 +98,8 @@ public ValueAnchor RegisterStrin /// Pointer path to the address of the string pointer in memory. /// Settings to read the string. /// An anchor for the value at the specified address. - public ValueAnchor RegisterStringAnchor(PointerPath pointerPath, - StringSettings stringSettings) + public ValueAnchor RegisterStringPointerAnchor( + PointerPath pointerPath, StringSettings stringSettings) { var memoryAdapter = new StringPointerMemoryAdapter( new PointerPathResolver(pointerPath), stringSettings); diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs new file mode 100644 index 0000000..63c4283 --- /dev/null +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs @@ -0,0 +1,191 @@ +using System.Globalization; +using MindControl.Results; +using NUnit.Framework; + +namespace MindControl.Test.ProcessMemoryTests; + +/// Tests the methods of related to value anchors. +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryAnchorTest : BaseProcessMemoryTest +{ + #region Primitive type anchors + + /// + /// Tests with the static address of the int value in the + /// target app and reads the value. The result should be the initial int value. + /// + [Test] + public void ReadIntAtStaticAddressTest() + { + using var anchor = TestProcessMemory!.RegisterAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); + var readResult = anchor.Read(); + Assert.That(readResult.IsSuccess, Is.True); + Assert.That(readResult.Value, Is.EqualTo(InitialIntValue)); + } + + /// + /// Tests with a pointer path to the int value in the + /// target app and reads the value. The result should be the initial int value. + /// + [Test] + public void ReadIntAtPointerPathTest() + { + using var anchor = TestProcessMemory!.RegisterAnchor(GetPointerPathForValueAtIndex(IndexOfOutputInt)); + var readResult = anchor.Read(); + Assert.That(readResult.IsSuccess, Is.True); + Assert.That(readResult.Value, Is.EqualTo(InitialIntValue)); + } + + /// + /// Tests with the static address of the int value in the + /// target app. Before the app outputs values, writes a new int value. The output should contain the value written. + /// + [Test] + public void WriteIntTest() + { + ProceedToNextStep(); + int newValue = 1234567; + using var anchor = TestProcessMemory!.RegisterAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); + var writeResult = anchor.Write(newValue); + Assert.That(writeResult.IsSuccess, Is.True); + ProceedToNextStep(); + AssertFinalResults(IndexOfOutputInt, newValue.ToString(CultureInfo.InvariantCulture)); + } + + /// + /// Tests with an address of 1, which is not readable. + /// When trying to read the value, the result should be a . + /// + [Test] + public void ReadIntWithOutOfRangeAddressTest() + { + using var anchor = TestProcessMemory!.RegisterAnchor(1); + var readResult = anchor.Read(); + Assert.That(readResult.IsSuccess, Is.False); + Assert.That(readResult.Error, Is.InstanceOf()); + } + + /// + /// Tests with an address of 1, which is not writeable. + /// When trying to write the value, the result should be a . + /// + [Test] + public void WriteIntWithOutOfRangeAddressTest() + { + using var anchor = TestProcessMemory!.RegisterAnchor(1); + var writeResult = anchor.Write(1234567); + Assert.That(writeResult.IsSuccess, Is.False); + Assert.That(writeResult.Error, Is.InstanceOf()); + } + + #endregion + + #region Byte array anchors + + /// + /// Tests with the static address of the output + /// byte array in the target app and reads the value. The result should be the initial byte array. + /// + [Test] + public void ReadBytesAtStaticAddressTest() + { + var address = TestProcessMemory!.EvaluateMemoryAddress( + GetPointerPathForValueAtIndex(IndexOfOutputByteArray)).Value; + using var anchor = TestProcessMemory.RegisterByteArrayAnchor(address, InitialByteArrayValue.Length); + var readResult = anchor.Read(); + Assert.That(readResult.IsSuccess, Is.True); + Assert.That(readResult.Value, Is.EqualTo(InitialByteArrayValue)); + } + + /// + /// Tests with a pointer path to the output + /// byte array in the target app and reads the value. The result should be the initial byte array. + /// + [Test] + public void ReadBytesAtPointerPathTest() + { + using var anchor = TestProcessMemory!.RegisterByteArrayAnchor( + GetPointerPathForValueAtIndex(IndexOfOutputByteArray), InitialByteArrayValue.Length); + var readResult = anchor.Read(); + Assert.That(readResult.IsSuccess, Is.True); + Assert.That(readResult.Value, Is.EqualTo(InitialByteArrayValue)); + } + + /// + /// Tests with a pointer path to the output + /// byte array in the target app. Before the app outputs values, writes a new value. The output should contain the + /// new value. + /// + [Test] + public void WriteBytesTest() + { + ProceedToNextStep(); + var newValue = new byte[] { 14, 24, 34, 44 }; + using var anchor = TestProcessMemory!.RegisterByteArrayAnchor( + GetPointerPathForValueAtIndex(IndexOfOutputByteArray), InitialByteArrayValue.Length); + var writeResult = anchor.Write(newValue); + Assert.That(writeResult.IsSuccess, Is.True); + ProceedToNextStep(); + AssertFinalResults(IndexOfOutputByteArray, "14,24,34,44"); + } + + #endregion + + #region String anchors + + /// + /// Tests with the static address of + /// the output string pointer in the target app and reads the value. The result should be the initial string. + /// + [Test] + public void ReadStringPointerAtStaticAddressTest() + { + using var anchor = TestProcessMemory!.RegisterStringPointerAnchor( + GetAddressForValueAtIndex(IndexOfOutputString), GetDotNetStringSettings()); + var readResult = anchor.Read(); + Assert.That(readResult.IsSuccess, Is.True); + Assert.That(readResult.Value, Is.EqualTo(InitialStringValue)); + } + + /// + /// Tests with a pointer path to + /// the output string pointer in the target app and reads the value. The result should be the initial string. + /// + [Test] + public void ReadStringPointerAtPointerPathTest() + { + using var anchor = TestProcessMemory!.RegisterStringPointerAnchor( + GetPointerPathForValueAtIndex(IndexOfOutputString), GetDotNetStringSettings()); + var readResult = anchor.Read(); + Assert.That(readResult.IsSuccess, Is.True); + Assert.That(readResult.Value, Is.EqualTo(InitialStringValue)); + } + + /// + /// Tests with the static address of + /// the output string pointer in the target app. Before the app outputs values, writes a new value. The operation + /// should fail because it is not supported. + /// + [Test] + public void WriteStringPointerTest() + { + ProceedToNextStep(); + var newValue = "Hello, world!"; + using var anchor = TestProcessMemory!.RegisterStringPointerAnchor( + GetAddressForValueAtIndex(IndexOfOutputString), GetDotNetStringSettings()); + var writeResult = anchor.Write(newValue); + Assert.That(writeResult.IsSuccess, Is.False); + } + + #endregion +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ProcessMemoryAnchorTestX86 : ProcessMemoryAnchorTest +{ + /// Gets a value indicating if the process is 64-bit. + protected override bool Is64Bit => false; +} \ No newline at end of file From 5d9b87dc0f743ab74890088df3691933b1fc94ca Mon Sep 17 00:00:00 2001 From: Doublevil Date: Tue, 23 Jul 2024 07:58:26 +0200 Subject: [PATCH 44/66] Freezer draft --- src/MindControl/Anchors/IValueAnchor.cs | 15 +++- src/MindControl/Anchors/IValueFreezer.cs | 33 +++++++++ src/MindControl/Anchors/TimerValueFreezer.cs | 70 +++++++++++++++++++ src/MindControl/Anchors/ValueAnchor.cs | 62 ++++++++++++++-- .../ProcessMemory/ProcessMemory.Anchors.cs | 12 +++- src/MindControl/Results/ValueAnchorFailure.cs | 20 ++++++ 6 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 src/MindControl/Anchors/IValueFreezer.cs create mode 100644 src/MindControl/Anchors/TimerValueFreezer.cs create mode 100644 src/MindControl/Results/ValueAnchorFailure.cs diff --git a/src/MindControl/Anchors/IValueAnchor.cs b/src/MindControl/Anchors/IValueAnchor.cs index ce9dcf4..b9d942a 100644 --- a/src/MindControl/Anchors/IValueAnchor.cs +++ b/src/MindControl/Anchors/IValueAnchor.cs @@ -1,7 +1,16 @@ -namespace MindControl.Anchors; +using MindControl.Results; + +namespace MindControl.Anchors; /// Provides methods to manipulate and track a specific value in memory. -public interface IValueAnchor : IDisposable +public interface IValueAnchor : IDisposable { - + /// Reads the value in the memory of the target process. + /// A result holding either the value read from memory, or a failure. + Result Read(); + + /// Writes the value to the memory of the target process. + /// Value to write to memory. + /// A result indicating success or failure. + public Result Write(TValue value); } \ No newline at end of file diff --git a/src/MindControl/Anchors/IValueFreezer.cs b/src/MindControl/Anchors/IValueFreezer.cs new file mode 100644 index 0000000..69ac315 --- /dev/null +++ b/src/MindControl/Anchors/IValueFreezer.cs @@ -0,0 +1,33 @@ +using MindControl.Results; + +namespace MindControl.Anchors; + +/// Event arguments used when a freeze operation fails. +/// Failure that occurred when trying to freeze the value. +/// Type of the failure. +public class FreezeFailureEventArgs(TFailure Failure) : EventArgs; + +/// +/// Attaches to a to prevent a memory area from changing +/// value. +/// +/// Type of the value to freeze. +/// Type of the failure that can occur when reading the value. +/// Type of the failure that can occur when writing the value. +public interface IValueFreezer : IDisposable +{ + /// Event raised when a freeze operation fails. + event EventHandler> FreezeFailed; + + /// Gets a boolean value indicating if a value is currently being frozen. + bool IsFreezing { get; } + + /// Freezes the memory area that the anchor is attached to, preventing its value from changing, until + /// either this instance is disposed, or is called. + /// Anchor holding the memory value to freeze. + /// A result indicating success or failure. + Result StartFreezing(IValueAnchor anchor); + + /// Interrupts freezing if had been previously called. + void Unfreeze(); +} \ No newline at end of file diff --git a/src/MindControl/Anchors/TimerValueFreezer.cs b/src/MindControl/Anchors/TimerValueFreezer.cs new file mode 100644 index 0000000..cf9aefa --- /dev/null +++ b/src/MindControl/Anchors/TimerValueFreezer.cs @@ -0,0 +1,70 @@ +using MindControl.Results; +using MindControl.State; + +namespace MindControl.Anchors; + +/// +/// Provides methods to freeze a value in memory for a specific duration. +/// This implementation uses a timer to write the value to memory at regular intervals. +/// +public class TimerValueFreezer(TValue value, TimeSpan timerInterval) + : IValueFreezer +{ + private IValueAnchor? _anchor; + private PrecisionTimer? _timer; + + /// Event raised when a freeze operation fails. + public event EventHandler>? FreezeFailed; + + /// Gets a boolean value indicating if a value is currently being frozen. + public bool IsFreezing => _timer != null; + + /// Freezes the memory area that the anchor is attached to, preventing its value from changing, until + /// either this instance is disposed, or is called. + /// Anchor holding the memory value to freeze. + /// A result indicating success or failure. + public Result StartFreezing(IValueAnchor anchor) + { + if (IsFreezing) + return "This instance is already freezing a value."; + + _anchor = anchor; + _timer = new PrecisionTimer(timerInterval); + _timer.Tick += OnTimerTick; + _timer.Start(); + + return Result.Success; + } + + /// + /// Callback for the timer tick event. Writes the value to the anchor, and raises the + /// event if the write operation fails. + /// + private void OnTimerTick(object? sender, EventArgs e) + { + if (_anchor == null) + return; + + var result = _anchor.Write(value); + if (result.IsFailure) + FreezeFailed?.Invoke(this, new FreezeFailureEventArgs(result.Error)); + } + + /// Interrupts freezing if had been previously called. + public void Unfreeze() + { + if (!IsFreezing) + return; + + _timer?.Stop(); + _timer = null; + _anchor = null; + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged + /// resources. + public void Dispose() + { + Unfreeze(); + } +} \ No newline at end of file diff --git a/src/MindControl/Anchors/ValueAnchor.cs b/src/MindControl/Anchors/ValueAnchor.cs index e48ef58..332b34d 100644 --- a/src/MindControl/Anchors/ValueAnchor.cs +++ b/src/MindControl/Anchors/ValueAnchor.cs @@ -10,20 +10,72 @@ namespace MindControl.Anchors; /// Type of the failure that can occur when writing the value. public class ValueAnchor (IMemoryAdapter memoryAdapter, ProcessMemory processMemory) - : IValueAnchor + : IValueAnchor { + private bool _isDisposed; + private IValueFreezer? _valueFreezer; + + /// Gets a boolean value indicating if a value is currently being frozen. + public bool IsFrozen => _valueFreezer?.IsFreezing == true; + /// Reads the value in the memory of the target process. /// A result holding either the value read from memory, or a failure. - public Result Read() => memoryAdapter.Read(processMemory); - + public Result Read() + { + if (_isDisposed) + return new ValueAnchorFailureOnDisposedInstance(); + + var result = memoryAdapter.Read(processMemory); + if (result.IsFailure) + return new ValueAnchorFailure(result.Error); + return result.Value; + } + /// Writes the value to the memory of the target process. /// Value to write to memory. /// A result indicating success or failure. - public Result Write(TValue value) => memoryAdapter.Write(processMemory, value); + public Result Write(TValue value) + { + if (_isDisposed) + return new ValueAnchorFailureOnDisposedInstance(); + + var result = memoryAdapter.Write(processMemory, value); + if (result.IsFailure) + return new ValueAnchorFailure(result.Error); + return Result.Success; + } + + /// Freezes the memory area that the anchor is attached to, preventing its value from changing, until + /// either this instance is disposed, or is called. + public Result Freeze(TValue value) + { + if (_isDisposed) + return "This instance has been disposed."; + + if (_valueFreezer?.IsFreezing == true) + _valueFreezer.Dispose(); + + _valueFreezer = new TimerValueFreezer(value, + TimeSpan.FromSeconds(1 / 145f)); + var result = _valueFreezer.StartFreezing(this); + if (result.IsFailure) + return result.Error; + + return Result.Success; + } + + /// Interrupts freezing if had been previously called. + public void Unfreeze() => _valueFreezer?.Unfreeze(); - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged + /// resources. public void Dispose() { + if (_isDisposed) + return; + + _isDisposed = true; + _valueFreezer?.Dispose(); processMemory.RemoveAnchor(this); } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs b/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs index 48fedb1..8159c72 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs @@ -6,7 +6,7 @@ namespace MindControl; // This partial class implements methods related to anchors public partial class ProcessMemory { - private readonly List _anchors = new(); + private readonly List _anchors = new(); /// /// Builds and returns an anchor for a value of type at the specified address. Anchors @@ -108,7 +108,15 @@ public ValueAnchor RegisterStrin return anchor; } + public TAnchor RegisterAnchor( + Func anchorFactory) where TAnchor: IValueAnchor + { + var anchor = anchorFactory(this); + _anchors.Add(anchor); + return anchor; + } + /// Removes an anchor from the list of anchors. Designed to be called by the anchor on disposal. /// Anchor to remove from the list of anchors. - internal void RemoveAnchor(IValueAnchor anchor) => _anchors.Remove(anchor); + internal void RemoveAnchor(IDisposable anchor) => _anchors.Remove(anchor); } \ No newline at end of file diff --git a/src/MindControl/Results/ValueAnchorFailure.cs b/src/MindControl/Results/ValueAnchorFailure.cs new file mode 100644 index 0000000..d63b6a4 --- /dev/null +++ b/src/MindControl/Results/ValueAnchorFailure.cs @@ -0,0 +1,20 @@ +namespace MindControl.Results; + +/// Represents a failure when reading or writing a value anchor. +public abstract record ValueAnchorFailure; + +/// Represents a failure when trying to read or write a value anchor on a disposed instance. +public record ValueAnchorFailureOnDisposedInstance : ValueAnchorFailure +{ + /// Returns a string that represents the current object. + public override string ToString() => "The anchor is disposed and cannot be used anymore."; +} + +/// Represents a failure when trying to read or write a value anchor with invalid arguments. +/// Underlying failure that occurred when trying to read or write the value. +public record ValueAnchorFailure(TFailure Failure) : ValueAnchorFailure +{ + /// Returns a string that represents the current object. + public override string ToString() => Failure?.ToString() + ?? "An unspecified failure occurred when trying to read or write the value."; +} \ No newline at end of file From bb088d0921c496719192fcb7abf5f06b86375fee Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sun, 11 Aug 2024 16:29:03 +0200 Subject: [PATCH 45/66] Reworked freezer implementation --- src/MindControl/Anchors/IValueAnchor.cs | 16 --- src/MindControl/Anchors/IValueFreezer.cs | 33 ----- src/MindControl/Anchors/ThreadValueFreezer.cs | 62 +++++++++ src/MindControl/Anchors/TimerValueFreezer.cs | 92 ++++++++----- src/MindControl/Anchors/ValueAnchor.cs | 61 ++------ .../ProcessMemory/ProcessMemory.Anchors.cs | 50 ++----- .../ProcessMemory/ProcessMemory.cs | 4 - src/MindControl/Results/ValueAnchorFailure.cs | 20 --- .../ProcessMemoryAnchorTest.cs | 130 +++++++++++++----- 9 files changed, 237 insertions(+), 231 deletions(-) delete mode 100644 src/MindControl/Anchors/IValueAnchor.cs delete mode 100644 src/MindControl/Anchors/IValueFreezer.cs create mode 100644 src/MindControl/Anchors/ThreadValueFreezer.cs delete mode 100644 src/MindControl/Results/ValueAnchorFailure.cs diff --git a/src/MindControl/Anchors/IValueAnchor.cs b/src/MindControl/Anchors/IValueAnchor.cs deleted file mode 100644 index b9d942a..0000000 --- a/src/MindControl/Anchors/IValueAnchor.cs +++ /dev/null @@ -1,16 +0,0 @@ -using MindControl.Results; - -namespace MindControl.Anchors; - -/// Provides methods to manipulate and track a specific value in memory. -public interface IValueAnchor : IDisposable -{ - /// Reads the value in the memory of the target process. - /// A result holding either the value read from memory, or a failure. - Result Read(); - - /// Writes the value to the memory of the target process. - /// Value to write to memory. - /// A result indicating success or failure. - public Result Write(TValue value); -} \ No newline at end of file diff --git a/src/MindControl/Anchors/IValueFreezer.cs b/src/MindControl/Anchors/IValueFreezer.cs deleted file mode 100644 index 69ac315..0000000 --- a/src/MindControl/Anchors/IValueFreezer.cs +++ /dev/null @@ -1,33 +0,0 @@ -using MindControl.Results; - -namespace MindControl.Anchors; - -/// Event arguments used when a freeze operation fails. -/// Failure that occurred when trying to freeze the value. -/// Type of the failure. -public class FreezeFailureEventArgs(TFailure Failure) : EventArgs; - -/// -/// Attaches to a to prevent a memory area from changing -/// value. -/// -/// Type of the value to freeze. -/// Type of the failure that can occur when reading the value. -/// Type of the failure that can occur when writing the value. -public interface IValueFreezer : IDisposable -{ - /// Event raised when a freeze operation fails. - event EventHandler> FreezeFailed; - - /// Gets a boolean value indicating if a value is currently being frozen. - bool IsFreezing { get; } - - /// Freezes the memory area that the anchor is attached to, preventing its value from changing, until - /// either this instance is disposed, or is called. - /// Anchor holding the memory value to freeze. - /// A result indicating success or failure. - Result StartFreezing(IValueAnchor anchor); - - /// Interrupts freezing if had been previously called. - void Unfreeze(); -} \ No newline at end of file diff --git a/src/MindControl/Anchors/ThreadValueFreezer.cs b/src/MindControl/Anchors/ThreadValueFreezer.cs new file mode 100644 index 0000000..c90efc1 --- /dev/null +++ b/src/MindControl/Anchors/ThreadValueFreezer.cs @@ -0,0 +1,62 @@ +namespace MindControl.Anchors; + +/// Provides methods to freeze a value in memory, using a thread that constantly writes the value. +/// Type of the value to freeze. +/// Type of the failure that can occur when reading the value. +/// Type of the failure that can occur when writing the value. +public class ThreadValueFreezer : IDisposable +{ + private readonly ValueAnchor _anchor; + private readonly TValue _value; + private bool _disposed; + + /// Event raised when a freeze operation fails. + public event EventHandler>? FreezeFailed; + + /// + /// Freezes a value in memory, using a thread that constantly writes the target value. + /// + /// Anchor holding the memory value to freeze. + /// Value to freeze in memory. + public ThreadValueFreezer(ValueAnchor anchor, TValue value) + { + _anchor = anchor; + _value = value; + new Thread(WriteForever).Start(); + } + + /// + /// Writes the value to the anchor until this instance is disposed, and raises the event + /// whenever the write operation fails. + /// + private void WriteForever() + { + while (!_disposed) + { + var result = _anchor.Write(_value); + if (result.IsFailure) + FreezeFailed?.Invoke(this, new FreezeFailureEventArgs(result.Error)); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged + /// resources. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// Disposes the timer and unsubscribes from the tick event. + /// Whether the object is being disposed. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + FreezeFailed = null; + + _disposed = true; + } +} \ No newline at end of file diff --git a/src/MindControl/Anchors/TimerValueFreezer.cs b/src/MindControl/Anchors/TimerValueFreezer.cs index cf9aefa..215c0ee 100644 --- a/src/MindControl/Anchors/TimerValueFreezer.cs +++ b/src/MindControl/Anchors/TimerValueFreezer.cs @@ -1,39 +1,40 @@ -using MindControl.Results; -using MindControl.State; +using MindControl.State; namespace MindControl.Anchors; +/// Event arguments used when a freeze operation fails. +/// Failure that occurred when trying to freeze the value. +/// Type of the failure. +public class FreezeFailureEventArgs(TFailure Failure) : EventArgs; + /// -/// Provides methods to freeze a value in memory for a specific duration. -/// This implementation uses a timer to write the value to memory at regular intervals. +/// Provides methods to freeze a value in memory, using a timer to write the value at regular intervals. /// -public class TimerValueFreezer(TValue value, TimeSpan timerInterval) - : IValueFreezer +public class TimerValueFreezer : IDisposable { - private IValueAnchor? _anchor; - private PrecisionTimer? _timer; - - /// Event raised when a freeze operation fails. - public event EventHandler>? FreezeFailed; + private readonly PrecisionTimer _timer; + private readonly ValueAnchor _anchor; + private readonly TValue _value; + private bool _isTicking; + private bool _disposed; - /// Gets a boolean value indicating if a value is currently being frozen. - public bool IsFreezing => _timer != null; + /// Event raised when a freeze operation fails. + public event EventHandler>? FreezeFailed; - /// Freezes the memory area that the anchor is attached to, preventing its value from changing, until - /// either this instance is disposed, or is called. + /// + /// Freezes a value in memory, using a timer to write the value at regular intervals. + /// /// Anchor holding the memory value to freeze. - /// A result indicating success or failure. - public Result StartFreezing(IValueAnchor anchor) + /// Value to freeze in memory. + /// Interval at which the value should be written to memory. + public TimerValueFreezer(ValueAnchor anchor, TValue value, + TimeSpan timerInterval) { - if (IsFreezing) - return "This instance is already freezing a value."; - _anchor = anchor; + _value = value; _timer = new PrecisionTimer(timerInterval); _timer.Tick += OnTimerTick; _timer.Start(); - - return Result.Success; } /// @@ -42,29 +43,44 @@ public Result StartFreezing(IValueAnchor private void OnTimerTick(object? sender, EventArgs e) { - if (_anchor == null) - return; - - var result = _anchor.Write(value); - if (result.IsFailure) - FreezeFailed?.Invoke(this, new FreezeFailureEventArgs(result.Error)); - } - - /// Interrupts freezing if had been previously called. - public void Unfreeze() - { - if (!IsFreezing) + if (_isTicking) return; - _timer?.Stop(); - _timer = null; - _anchor = null; + _isTicking = true; + try + { + var result = _anchor.Write(_value); + if (result.IsFailure) + FreezeFailed?.Invoke(this, new FreezeFailureEventArgs(result.Error)); + } + finally + { + _isTicking = false; + } } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged /// resources. public void Dispose() { - Unfreeze(); + Dispose(true); + GC.SuppressFinalize(this); + } + + /// Disposes the timer and unsubscribes from the tick event. + /// Whether the object is being disposed. + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _timer.Stop(); + _timer.Tick -= OnTimerTick; + FreezeFailed = null; + } + + _disposed = true; } } \ No newline at end of file diff --git a/src/MindControl/Anchors/ValueAnchor.cs b/src/MindControl/Anchors/ValueAnchor.cs index 332b34d..2d5e4e4 100644 --- a/src/MindControl/Anchors/ValueAnchor.cs +++ b/src/MindControl/Anchors/ValueAnchor.cs @@ -10,72 +10,37 @@ namespace MindControl.Anchors; /// Type of the failure that can occur when writing the value. public class ValueAnchor (IMemoryAdapter memoryAdapter, ProcessMemory processMemory) - : IValueAnchor { - private bool _isDisposed; - private IValueFreezer? _valueFreezer; - - /// Gets a boolean value indicating if a value is currently being frozen. - public bool IsFrozen => _valueFreezer?.IsFreezing == true; - /// Reads the value in the memory of the target process. /// A result holding either the value read from memory, or a failure. - public Result Read() + public Result Read() { - if (_isDisposed) - return new ValueAnchorFailureOnDisposedInstance(); - var result = memoryAdapter.Read(processMemory); if (result.IsFailure) - return new ValueAnchorFailure(result.Error); + return result.Error; return result.Value; } /// Writes the value to the memory of the target process. /// Value to write to memory. /// A result indicating success or failure. - public Result Write(TValue value) + public Result Write(TValue value) { - if (_isDisposed) - return new ValueAnchorFailureOnDisposedInstance(); - var result = memoryAdapter.Write(processMemory, value); - if (result.IsFailure) - return new ValueAnchorFailure(result.Error); - return Result.Success; - } - - /// Freezes the memory area that the anchor is attached to, preventing its value from changing, until - /// either this instance is disposed, or is called. - public Result Freeze(TValue value) - { - if (_isDisposed) - return "This instance has been disposed."; - - if (_valueFreezer?.IsFreezing == true) - _valueFreezer.Dispose(); - - _valueFreezer = new TimerValueFreezer(value, - TimeSpan.FromSeconds(1 / 145f)); - var result = _valueFreezer.StartFreezing(this); if (result.IsFailure) return result.Error; - - return Result.Success; + return Result.Success; } - - /// Interrupts freezing if had been previously called. - public void Unfreeze() => _valueFreezer?.Unfreeze(); - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged - /// resources. - public void Dispose() + /// Freezes the memory area that the anchor is attached to, constantly overwriting its value, until the + /// resulting instance is disposed. + /// A instance that can be disposed to + /// stop freezing the value, and provides a subscribable event raised when a recurrent write operation fails. + /// + public TimerValueFreezer Freeze(TValue value) { - if (_isDisposed) - return; - - _isDisposed = true; - _valueFreezer?.Dispose(); - processMemory.RemoveAnchor(this); + // Use an arbitrary 150 updates per second as the default interval (a bit more than the standard 144FPS). + return new TimerValueFreezer(this, value, + TimeSpan.FromSeconds(1 / 150f)); } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs b/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs index 8159c72..d76dc3c 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs @@ -6,8 +6,6 @@ namespace MindControl; // This partial class implements methods related to anchors public partial class ProcessMemory { - private readonly List _anchors = new(); - /// /// Builds and returns an anchor for a value of type at the specified address. Anchors /// allow you to track and manipulate a specific value in memory. When not needed anymore, anchors should be @@ -16,13 +14,11 @@ public partial class ProcessMemory /// Address of the value in memory. /// Type of the value to read and write. /// An anchor for the value at the specified address. - public ValueAnchor RegisterAnchor(UIntPtr address) + public ValueAnchor GetAnchor(UIntPtr address) where T : struct { var memoryAdapter = new GenericMemoryAdapter(new LiteralAddressResolver(address)); - var anchor = new ValueAnchor(memoryAdapter, this); - _anchors.Add(anchor); - return anchor; + return new ValueAnchor(memoryAdapter, this); } /// @@ -33,13 +29,11 @@ public ValueAnchor RegisterAnchor(UIntPtr addre /// Pointer path to the address of the value in memory. /// Type of the value to read and write. /// An anchor for the value at the specified address. - public ValueAnchor RegisterAnchor(PointerPath pointerPath) + public ValueAnchor GetAnchor(PointerPath pointerPath) where T : struct { var memoryAdapter = new GenericMemoryAdapter(new PointerPathResolver(pointerPath)); - var anchor = new ValueAnchor(memoryAdapter, this); - _anchors.Add(anchor); - return anchor; + return new ValueAnchor(memoryAdapter, this); } /// @@ -49,12 +43,10 @@ public ValueAnchor RegisterAnchor(PointerPath p /// Address of target byte array in memory. /// Size of the target byte array. /// An anchor for the array at the specified address. - public ValueAnchor RegisterByteArrayAnchor(UIntPtr address, int size) + public ValueAnchor GetByteArrayAnchor(UIntPtr address, int size) { var memoryAdapter = new ByteArrayMemoryAdapter(new LiteralAddressResolver(address), size); - var anchor = new ValueAnchor(memoryAdapter, this); - _anchors.Add(anchor); - return anchor; + return new ValueAnchor(memoryAdapter, this); } /// @@ -64,13 +56,11 @@ public ValueAnchor RegisterByteArrayAnchor(UI /// Pointer path to the address of the target array in memory. /// Size of the target byte array. /// An anchor for the array at the specified address. - public ValueAnchor RegisterByteArrayAnchor(PointerPath pointerPath, int size) + public ValueAnchor GetByteArrayAnchor(PointerPath pointerPath, int size) { var memoryAdapter = new ByteArrayMemoryAdapter( new PointerPathResolver(pointerPath), size); - var anchor = new ValueAnchor(memoryAdapter, this); - _anchors.Add(anchor); - return anchor; + return new ValueAnchor(memoryAdapter, this); } /// @@ -81,13 +71,11 @@ public ValueAnchor RegisterByteArrayAnchor(Po /// Address of the string pointer in memory. /// Settings to read the string. /// An anchor for the value at the specified address. - public ValueAnchor RegisterStringPointerAnchor(UIntPtr address, + public ValueAnchor GetStringPointerAnchor(UIntPtr address, StringSettings stringSettings) { var memoryAdapter = new StringPointerMemoryAdapter(new LiteralAddressResolver(address), stringSettings); - var anchor = new ValueAnchor(memoryAdapter, this); - _anchors.Add(anchor); - return anchor; + return new ValueAnchor(memoryAdapter, this); } /// @@ -98,25 +86,11 @@ public ValueAnchor RegisterStrin /// Pointer path to the address of the string pointer in memory. /// Settings to read the string. /// An anchor for the value at the specified address. - public ValueAnchor RegisterStringPointerAnchor( + public ValueAnchor GetStringPointerAnchor( PointerPath pointerPath, StringSettings stringSettings) { var memoryAdapter = new StringPointerMemoryAdapter( new PointerPathResolver(pointerPath), stringSettings); - var anchor = new ValueAnchor(memoryAdapter, this); - _anchors.Add(anchor); - return anchor; + return new ValueAnchor(memoryAdapter, this); } - - public TAnchor RegisterAnchor( - Func anchorFactory) where TAnchor: IValueAnchor - { - var anchor = anchorFactory(this); - _anchors.Add(anchor); - return anchor; - } - - /// Removes an anchor from the list of anchors. Designed to be called by the anchor on disposal. - /// Anchor to remove from the list of anchors. - internal void RemoveAnchor(IDisposable anchor) => _anchors.Remove(anchor); } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.cs b/src/MindControl/ProcessMemory/ProcessMemory.cs index ed7be5b..f6b6bb4 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.cs @@ -189,10 +189,6 @@ private void Detach() { if (IsAttached) { - // Dispose all anchors to prevent them from accessing memory after detaching - foreach (var anchor in _anchors) - anchor.Dispose(); - IsAttached = false; _process.Exited -= OnProcessExited; ProcessDetached?.Invoke(this, EventArgs.Empty); diff --git a/src/MindControl/Results/ValueAnchorFailure.cs b/src/MindControl/Results/ValueAnchorFailure.cs deleted file mode 100644 index d63b6a4..0000000 --- a/src/MindControl/Results/ValueAnchorFailure.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MindControl.Results; - -/// Represents a failure when reading or writing a value anchor. -public abstract record ValueAnchorFailure; - -/// Represents a failure when trying to read or write a value anchor on a disposed instance. -public record ValueAnchorFailureOnDisposedInstance : ValueAnchorFailure -{ - /// Returns a string that represents the current object. - public override string ToString() => "The anchor is disposed and cannot be used anymore."; -} - -/// Represents a failure when trying to read or write a value anchor with invalid arguments. -/// Underlying failure that occurred when trying to read or write the value. -public record ValueAnchorFailure(TFailure Failure) : ValueAnchorFailure -{ - /// Returns a string that represents the current object. - public override string ToString() => Failure?.ToString() - ?? "An unspecified failure occurred when trying to read or write the value."; -} \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs index 63c4283..b92832c 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs @@ -1,4 +1,5 @@ using System.Globalization; +using MindControl.Anchors; using MindControl.Results; using NUnit.Framework; @@ -11,41 +12,41 @@ public class ProcessMemoryAnchorTest : BaseProcessMemoryTest #region Primitive type anchors /// - /// Tests with the static address of the int value in the - /// target app and reads the value. The result should be the initial int value. + /// Tests with the static address of the int value in the target + /// app and reads the value. The result should be the initial int value. /// [Test] public void ReadIntAtStaticAddressTest() { - using var anchor = TestProcessMemory!.RegisterAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); + var anchor = TestProcessMemory!.GetAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); var readResult = anchor.Read(); Assert.That(readResult.IsSuccess, Is.True); Assert.That(readResult.Value, Is.EqualTo(InitialIntValue)); } /// - /// Tests with a pointer path to the int value in the - /// target app and reads the value. The result should be the initial int value. + /// Tests with a pointer path to the int value in the target + /// app and reads the value. The result should be the initial int value. /// [Test] public void ReadIntAtPointerPathTest() { - using var anchor = TestProcessMemory!.RegisterAnchor(GetPointerPathForValueAtIndex(IndexOfOutputInt)); + var anchor = TestProcessMemory!.GetAnchor(GetPointerPathForValueAtIndex(IndexOfOutputInt)); var readResult = anchor.Read(); Assert.That(readResult.IsSuccess, Is.True); Assert.That(readResult.Value, Is.EqualTo(InitialIntValue)); } /// - /// Tests with the static address of the int value in the - /// target app. Before the app outputs values, writes a new int value. The output should contain the value written. + /// Tests with the static address of the int value in the target + /// app. Before the app outputs values, writes a new int value. The output should contain the value written. /// [Test] public void WriteIntTest() { ProceedToNextStep(); int newValue = 1234567; - using var anchor = TestProcessMemory!.RegisterAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); + var anchor = TestProcessMemory!.GetAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); var writeResult = anchor.Write(newValue); Assert.That(writeResult.IsSuccess, Is.True); ProceedToNextStep(); @@ -53,58 +54,120 @@ public void WriteIntTest() } /// - /// Tests with an address of 1, which is not readable. + /// Tests with an address of 1, which is not readable. /// When trying to read the value, the result should be a . /// [Test] public void ReadIntWithOutOfRangeAddressTest() { - using var anchor = TestProcessMemory!.RegisterAnchor(1); + var anchor = TestProcessMemory!.GetAnchor(1); var readResult = anchor.Read(); Assert.That(readResult.IsSuccess, Is.False); Assert.That(readResult.Error, Is.InstanceOf()); } /// - /// Tests with an address of 1, which is not writeable. + /// Tests with an address of 1, which is not writeable. /// When trying to write the value, the result should be a . /// [Test] public void WriteIntWithOutOfRangeAddressTest() { - using var anchor = TestProcessMemory!.RegisterAnchor(1); + var anchor = TestProcessMemory!.GetAnchor(1); var writeResult = anchor.Write(1234567); Assert.That(writeResult.IsSuccess, Is.False); Assert.That(writeResult.Error, Is.InstanceOf()); } + /// + /// Tests the Freeze method of the ValueAnchor. + /// Before the app outputs values, freezes the int value to 1234567. The output should contain the frozen value, + /// even though the app changes the value before outputting it. + /// + [Test] + public void FreezeIntTest() + { + var anchor = TestProcessMemory!.GetAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); + using var freezer = anchor.Freeze(1234567); + ProceedToNextStep(); + Thread.Sleep(100); // Make sure the freezer has time to write the value to make the test consistent + ProceedToNextStep(); + AssertFinalResults(IndexOfOutputInt, "1234567"); + } + + /// + /// Tests the Dispose method of the default freezer implementation. + /// Before the app outputs values, freezes the int value to 1234567, and immediately dispose the freezer. The output + /// should contain the expected output value of the target app. + /// + [Test] + public void FreezeIntAndDisposeFreezerTest() + { + var anchor = TestProcessMemory!.GetAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); + var freezer = anchor.Freeze(1234567); + freezer.Dispose(); + ProceedToNextStep(); + ProceedToNextStep(); + AssertExpectedFinalResults(); + } + + /// + /// Tests (the thread-based freezer + /// implementation). + /// + [Test] + public void FreezeIntThreadTest() + { + var anchor = TestProcessMemory!.GetAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); + using var freezer = new ThreadValueFreezer(anchor, 1234567); + ProceedToNextStep(); + Thread.Sleep(100); // Make sure the freezer has time to write the value to make the test consistent + ProceedToNextStep(); + AssertFinalResults(IndexOfOutputInt, "1234567"); + } + + /// + /// Tests the Dispose method of (the + /// thread-based freezer implementation). + /// + [Test] + public void FreezeIntThreadAndDisposeTest() + { + var anchor = TestProcessMemory!.GetAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); + var freezer = new ThreadValueFreezer(anchor, 1234567); + freezer.Dispose(); + ProceedToNextStep(); + ProceedToNextStep(); + AssertExpectedFinalResults(); + } + #endregion #region Byte array anchors /// - /// Tests with the static address of the output - /// byte array in the target app and reads the value. The result should be the initial byte array. + /// Tests with the static address of the output byte + /// array in the target app and reads the value. The result should be the initial byte array. /// [Test] public void ReadBytesAtStaticAddressTest() { var address = TestProcessMemory!.EvaluateMemoryAddress( GetPointerPathForValueAtIndex(IndexOfOutputByteArray)).Value; - using var anchor = TestProcessMemory.RegisterByteArrayAnchor(address, InitialByteArrayValue.Length); + var anchor = TestProcessMemory.GetByteArrayAnchor(address, InitialByteArrayValue.Length); var readResult = anchor.Read(); Assert.That(readResult.IsSuccess, Is.True); Assert.That(readResult.Value, Is.EqualTo(InitialByteArrayValue)); } /// - /// Tests with a pointer path to the output - /// byte array in the target app and reads the value. The result should be the initial byte array. + /// Tests with a pointer path to the output byte + /// array in the target app and reads the value. The result should be the initial byte array. /// [Test] public void ReadBytesAtPointerPathTest() { - using var anchor = TestProcessMemory!.RegisterByteArrayAnchor( + var anchor = TestProcessMemory!.GetByteArrayAnchor( GetPointerPathForValueAtIndex(IndexOfOutputByteArray), InitialByteArrayValue.Length); var readResult = anchor.Read(); Assert.That(readResult.IsSuccess, Is.True); @@ -112,16 +175,16 @@ public void ReadBytesAtPointerPathTest() } /// - /// Tests with a pointer path to the output - /// byte array in the target app. Before the app outputs values, writes a new value. The output should contain the - /// new value. + /// Tests with a pointer path to the output byte + /// array in the target app. Before the app outputs values, writes a new value. The output should contain the new + /// value. /// [Test] public void WriteBytesTest() { ProceedToNextStep(); var newValue = new byte[] { 14, 24, 34, 44 }; - using var anchor = TestProcessMemory!.RegisterByteArrayAnchor( + var anchor = TestProcessMemory!.GetByteArrayAnchor( GetPointerPathForValueAtIndex(IndexOfOutputByteArray), InitialByteArrayValue.Length); var writeResult = anchor.Write(newValue); Assert.That(writeResult.IsSuccess, Is.True); @@ -134,13 +197,13 @@ public void WriteBytesTest() #region String anchors /// - /// Tests with the static address of - /// the output string pointer in the target app and reads the value. The result should be the initial string. + /// Tests with the static address of the + /// output string pointer in the target app and reads the value. The result should be the initial string. /// [Test] public void ReadStringPointerAtStaticAddressTest() { - using var anchor = TestProcessMemory!.RegisterStringPointerAnchor( + var anchor = TestProcessMemory!.GetStringPointerAnchor( GetAddressForValueAtIndex(IndexOfOutputString), GetDotNetStringSettings()); var readResult = anchor.Read(); Assert.That(readResult.IsSuccess, Is.True); @@ -148,13 +211,13 @@ public void ReadStringPointerAtStaticAddressTest() } /// - /// Tests with a pointer path to + /// Tests with a pointer path to /// the output string pointer in the target app and reads the value. The result should be the initial string. /// [Test] public void ReadStringPointerAtPointerPathTest() { - using var anchor = TestProcessMemory!.RegisterStringPointerAnchor( + var anchor = TestProcessMemory!.GetStringPointerAnchor( GetPointerPathForValueAtIndex(IndexOfOutputString), GetDotNetStringSettings()); var readResult = anchor.Read(); Assert.That(readResult.IsSuccess, Is.True); @@ -162,18 +225,17 @@ public void ReadStringPointerAtPointerPathTest() } /// - /// Tests with the static address of - /// the output string pointer in the target app. Before the app outputs values, writes a new value. The operation - /// should fail because it is not supported. + /// Tests with the static address of the + /// output string pointer in the target app. Before the app outputs values, writes a new value. The operation should + /// fail because it is not supported. /// [Test] public void WriteStringPointerTest() { ProceedToNextStep(); - var newValue = "Hello, world!"; - using var anchor = TestProcessMemory!.RegisterStringPointerAnchor( + var anchor = TestProcessMemory!.GetStringPointerAnchor( GetAddressForValueAtIndex(IndexOfOutputString), GetDotNetStringSettings()); - var writeResult = anchor.Write(newValue); + var writeResult = anchor.Write("Hello, world!"); Assert.That(writeResult.IsSuccess, Is.False); } From 5870955f00d9bb4e18bcb6d143f4e054ecc92477 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Wed, 21 Aug 2024 21:31:48 +0200 Subject: [PATCH 46/66] ValueWatcher implementation --- src/MindControl/Anchors/ValueAnchor.cs | 20 ++ src/MindControl/Anchors/ValueWatcher.cs | 189 ++++++++++++++ .../AnchorTests/ValueWatcherTest.cs | 244 ++++++++++++++++++ 3 files changed, 453 insertions(+) create mode 100644 src/MindControl/Anchors/ValueWatcher.cs create mode 100644 test/MindControl.Test/AnchorTests/ValueWatcherTest.cs diff --git a/src/MindControl/Anchors/ValueAnchor.cs b/src/MindControl/Anchors/ValueAnchor.cs index 2d5e4e4..bfaa3b6 100644 --- a/src/MindControl/Anchors/ValueAnchor.cs +++ b/src/MindControl/Anchors/ValueAnchor.cs @@ -43,4 +43,24 @@ public TimerValueFreezer Freeze(TValue valu return new TimerValueFreezer(this, value, TimeSpan.FromSeconds(1 / 150f)); } + + /// + /// Provides a instance that periodically reads the + /// value from the anchor and raises events when the value changes, until it is disposed. + /// + /// Target time interval between each read operation in the watcher. + /// A instance that periodically reads the + /// value from the anchor and raises events when the value changes, until it is disposed. + public ValueWatcher Watch(TimeSpan refreshInterval) + => new(this, refreshInterval); + + /// + /// Provides a instance that periodically reads the + /// value from the anchor and raises events when the value changes, until it is disposed. + /// + /// Target number of reads per second of the watcher. + /// A instance that periodically reads the + /// value from the anchor and raises events when the value changes, until it is disposed. + public ValueWatcher Watch(int updatesPerSecond) + => new(this, TimeSpan.FromSeconds(1f / updatesPerSecond)); } \ No newline at end of file diff --git a/src/MindControl/Anchors/ValueWatcher.cs b/src/MindControl/Anchors/ValueWatcher.cs new file mode 100644 index 0000000..f34ebba --- /dev/null +++ b/src/MindControl/Anchors/ValueWatcher.cs @@ -0,0 +1,189 @@ +using MindControl.State; + +namespace MindControl.Anchors; + +/// +/// Event arguments used when a value observed by a +/// changes. +/// +/// Last known value before the change. +/// New value freshly read after the change. +/// Type of the value that changed. +public class ValueChangedEventArgs(TValue? previousValue, TValue newValue) : EventArgs +{ + /// Gets the last known value before the change. + public TValue? PreviousValue => previousValue; + + /// Gets the new value freshly read after the change. + public TValue NewValue => newValue; +} + +/// +/// Event arguments used when a value observed by a +/// becomes unreadable (causes a read failure). This may happen when the target process frees or rearranges its memory, +/// or when the related instance is detached. +/// +/// Last value read before a read error occurred. +public class ValueLostEventArgs(TValue lastKnownValue) : EventArgs +{ + /// Gets the last value read before a read error occurred. + public TValue LastKnownValue => lastKnownValue; +} + +/// +/// Event arguments used when a value observed by a +/// is successfully read after being lost. +/// +/// New value freshly read. +/// Type of the value that was reacquired. +public class ValueReacquiredEventArgs(TValue newValue) : EventArgs +{ + /// Gets the new value freshly read. + public TValue NewValue => newValue; +} + +/// +/// Uses a timer to periodically read a value from a given anchor and raise events when the value changes. +/// +/// Type of the value held by the anchor. +/// Type of the failure that can occur when reading the value. +/// Type of the failure that can occur when writing the value. +public class ValueWatcher : IDisposable +{ + private readonly ValueAnchor _anchor; + private readonly PrecisionTimer _timer; + private bool _isDisposed; + private readonly SemaphoreSlim _updateLock = new(1, 1); + + /// Event raised when the value observed by the watcher changes. + public event EventHandler>? ValueChanged; + + /// + /// Event raised when the value observed by the watcher becomes unreadable (causes a read failure). This may happen + /// when the target process frees or rearranges its memory, or when the related instance + /// is detached. + /// + public event EventHandler>? ValueLost; + + /// + /// Event raised when the value observed by the watcher is successfully read after being lost. + /// + public event EventHandler>? ValueReacquired; + + /// Gets a value indicating whether the last read operation was successful. + public bool IsValueReadable { get; private set; } + + /// + /// Gets the last value read by the watcher. This value is updated every time the watcher successfully reads a new + /// value. Even if the value is lost, this property will still hold the last successfully read value. If the value + /// was never read successfully, this property will hold the default value of . + /// + public TValue? LastKnownValue { get; private set; } + + /// Gets the last time the value either changed, was lost, or reacquired after a loss. + public DateTime LastChangeTime { get; private set; } + + /// + /// Uses a timer to periodically read a value from a given anchor and raise events when the value changes. + /// + /// The anchor holding the value to watch. + /// Target time interval between each read operation. + /// Type of the value held by the anchor. + /// Type of the failure that can occur when reading the value. + /// Type of the failure that can occur when writing the value. + public ValueWatcher(ValueAnchor anchor, TimeSpan refreshInterval) + { + _anchor = anchor; + LastChangeTime = DateTime.Now; + LastKnownValue = default; + _timer = new PrecisionTimer(refreshInterval); + _timer.Tick += OnTimerTick; + _timer.Start(); + + // Build the initial state + UpdateState(false); + } + + /// + /// Callback for the timer tick event. Reads the value from the anchor and raises events when the value changes. + /// + private void OnTimerTick(object? sender, EventArgs e) => UpdateState(); + + /// + /// Reads the value from the anchor and raises events when the value changes. + /// This method is called automatically by the timer, but can also be called manually to force a read. + /// + public void UpdateState() => UpdateState(true); + + /// + /// Reads the value from the anchor and raises events when the value changes, unless the + /// parameter is set to false. + /// + /// Whether to raise events when the value changes. + private void UpdateState(bool issueEvents) + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + if (!_updateLock.Wait(0)) + { + // An update is already in progress. + // Wait for the update to finish, and return without updating (which would almost certainly be pointless). + // This ensures no concurrent updates, and also that manual callers of UpdateState can expect that the + // state is updated after the method returns. + _updateLock.Wait(); + _updateLock.Release(); + return; + } + + try + { + bool lastReadWasSuccessful = IsValueReadable && LastKnownValue != null; + var previousValue = LastKnownValue; + var result = _anchor.Read(); + IsValueReadable = result.IsSuccess; + if (IsValueReadable) + { + var newValue = result.Value; + bool valueChanged = newValue?.Equals(LastKnownValue) == false; + if (lastReadWasSuccessful && !valueChanged) + return; + + LastChangeTime = DateTime.Now; + LastKnownValue = newValue; + if (!issueEvents) + return; + + if (!lastReadWasSuccessful) + ValueReacquired?.Invoke(this, new ValueReacquiredEventArgs(newValue)); + if (valueChanged) + ValueChanged?.Invoke(this, new ValueChangedEventArgs(previousValue, newValue)); + } + else if (lastReadWasSuccessful) + { + LastChangeTime = DateTime.Now; + if (issueEvents && previousValue != null) + ValueLost?.Invoke(this, new ValueLostEventArgs(previousValue)); + } + } + finally + { + _updateLock.Release(); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged + /// resources. + public void Dispose() + { + if (_isDisposed) + return; + + _timer.Stop(); + _isDisposed = true; + _updateLock.Dispose(); + ValueChanged = null; + ValueLost = null; + ValueReacquired = null; + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/test/MindControl.Test/AnchorTests/ValueWatcherTest.cs b/test/MindControl.Test/AnchorTests/ValueWatcherTest.cs new file mode 100644 index 0000000..e7bc46e --- /dev/null +++ b/test/MindControl.Test/AnchorTests/ValueWatcherTest.cs @@ -0,0 +1,244 @@ +using MindControl.Anchors; +using MindControl.Results; +using MindControl.Test.ProcessMemoryTests; +using NUnit.Framework; + +namespace MindControl.Test.AnchorTests; + +/// Tests . +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ValueWatcherTest : BaseProcessMemoryTest +{ + /// Builds and returns an anchor instance that can be used to build a value watcher. + private ValueAnchor GetAnchorOnOutputInt() + => TestProcessMemory!.GetAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); + + /// Builds and returns a value watcher instance that can be used to test the class. + private ValueWatcher GetWatcherOnOutputInt(TimeSpan? refreshInterval = null) + => GetAnchorOnOutputInt().Watch(refreshInterval ?? DefaultRefreshInterval); + + /// Default interval for refreshing the value in the watcher. + private static readonly TimeSpan DefaultRefreshInterval = TimeSpan.FromSeconds(5); + + /// + /// Tests the constructor. + /// The constructor should set the watcher instance to an initial state by reading the value without raising events. + /// + [Test] + public void ReadableInitialStateTest() + { + var dtAtStart = DateTime.Now; + var watcher = GetWatcherOnOutputInt(); + watcher.ValueChanged += (_, _) => Assert.Fail("ValueChanged event should not be raised."); + watcher.ValueLost += (_, _) => Assert.Fail("ValueLost event should not be raised."); + watcher.ValueReacquired += (_, _) => Assert.Fail("ValueReacquired event should not be raised."); + + // Even if we update the state here, it should be the same as the initial state, so no events should be raised + watcher.UpdateState(); + watcher.Dispose(); + + Assert.That(watcher.IsValueReadable, Is.True); + Assert.That(watcher.LastKnownValue, Is.EqualTo(InitialIntValue)); + Assert.That(watcher.LastChangeTime, Is.EqualTo(dtAtStart).Within(TimeSpan.FromSeconds(10))); + } + + /// + /// Tests the constructor. + /// The watcher is set to an initial state where the value is unreadable. + /// + [Test] + public void UnreadableInitialStateTest() + { + var reservation = TestProcessMemory!.Reserve(16, false).Value; + var pointerPath = new PointerPath($"{reservation.Address:X},0"); + var watcher = TestProcessMemory.GetAnchor(pointerPath).Watch(DefaultRefreshInterval); + watcher.ValueChanged += (_, _) => Assert.Fail("ValueChanged event should not be raised."); + watcher.ValueLost += (_, _) => Assert.Fail("ValueLost event should not be raised."); + watcher.ValueReacquired += (_, _) => Assert.Fail("ValueReacquired event should not be raised."); + + watcher.UpdateState(); + watcher.Dispose(); + + Assert.That(watcher.IsValueReadable, Is.False); + Assert.That(watcher.LastKnownValue, Is.EqualTo(0)); // Default value for int + } + + /// + /// Tests the event. + /// Build a value watcher, let the process change the value, force an update, and check if the event is raised. + /// + [Test] + public void ValueChangedTest() + { + bool valueChangedCalled = false; + var watcher = GetWatcherOnOutputInt(); + watcher.ValueChanged += (_, args) => + { + if (valueChangedCalled) + Assert.Fail("ValueChanged event should not be raised more than once."); + + valueChangedCalled = true; + Assert.That(args.PreviousValue, Is.EqualTo(InitialIntValue)); + Assert.That(args.NewValue, Is.EqualTo(ExpectedFinalIntValue)); + }; + watcher.ValueLost += (_, _) => Assert.Fail("ValueLost event should not be raised."); + watcher.ValueReacquired += (_, _) => Assert.Fail("ValueReacquired event should not be raised."); + + ProceedToNextStep(); // The target process will change the value here + watcher.UpdateState(); + watcher.UpdateState(); // Update once more to make sure events are not raised multiple times + watcher.Dispose(); + + Assert.That(watcher.IsValueReadable, Is.True); + Assert.That(valueChangedCalled, Is.True); + } + + /// + /// Tests the event. + /// Build a value watcher with a pointer path, change a pointer in the path so that the address no longer resolves, + /// and check that the event gets raised after a state update. + /// + [Test] + public void ValueLostTest() + { + using var reservation = TestProcessMemory!.Reserve(16, false).Value; + TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnError(); + int targetValue = 46; + TestProcessMemory.Write(reservation.Address + 8, targetValue).ThrowOnError(); + var pointerPath = new PointerPath($"{reservation.Address:X},0"); + + var watcher = TestProcessMemory.GetAnchor(pointerPath).Watch(DefaultRefreshInterval); + bool valueLostCalled = false; + watcher.ValueChanged += (_, _) => Assert.Fail("ValueChanged event should not be raised."); + watcher.ValueLost += (_, args) => + { + if (valueLostCalled) + Assert.Fail("ValueLost event should not be raised more than once."); + + valueLostCalled = true; + Assert.That(args.LastKnownValue, Is.EqualTo(targetValue)); + }; + watcher.ValueReacquired += (_, _) => Assert.Fail("ValueReacquired event should not be raised."); + + Assert.That(watcher.IsValueReadable, Is.True); + Assert.That(watcher.LastKnownValue, Is.EqualTo(targetValue)); + + TestProcessMemory.Write(reservation.Address, 0).ThrowOnError(); // Sabotage the pointer path + + watcher.UpdateState(); + watcher.UpdateState(); // Update once more to make sure events are not raised multiple times + watcher.Dispose(); + + Assert.That(watcher.IsValueReadable, Is.False); + Assert.That(valueLostCalled, Is.True); + Assert.That(watcher.LastKnownValue, Is.EqualTo(targetValue)); + } + + /// + /// Tests the event. + /// Build a value watcher with a pointer path, change a pointer in the path so that the address no longer resolves, + /// update the state so that the value gets lost, and then repair the pointer path and update the state again. + /// The tested event should be raised exactly once. + /// + [Test] + public void ValueReacquiredTest() + { + using var reservation = TestProcessMemory!.Reserve(16, false).Value; + TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnError(); + int targetValue = 46; + TestProcessMemory.Write(reservation.Address + 8, targetValue).ThrowOnError(); + var pointerPath = new PointerPath($"{reservation.Address:X},0"); + + var watcher = TestProcessMemory.GetAnchor(pointerPath).Watch(DefaultRefreshInterval); + bool valueReacquiredCalled = false; + watcher.ValueChanged += (_, _) => Assert.Fail("ValueChanged event should not be raised."); + watcher.ValueReacquired += (_, args) => + { + if (valueReacquiredCalled) + Assert.Fail("ValueReacquired event should not be raised more than once."); + + valueReacquiredCalled = true; + Assert.That(args.NewValue, Is.EqualTo(targetValue)); + }; + + Assert.That(watcher.IsValueReadable, Is.True); + Assert.That(watcher.LastKnownValue, Is.EqualTo(targetValue)); + + TestProcessMemory.Write(reservation.Address, 0).ThrowOnError(); // Sabotage the pointer path + watcher.UpdateState(); // This should raise a ValueLost event + + TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnError(); // Repair the pointer path + watcher.UpdateState(); // This should raise a ValueReacquired event + watcher.UpdateState(); // Update once more to make sure events are not raised multiple times + watcher.Dispose(); + + Assert.That(watcher.IsValueReadable, Is.True); + Assert.That(valueReacquiredCalled, Is.True); + Assert.That(watcher.LastKnownValue, Is.EqualTo(targetValue)); + } + + /// + /// Tests the and + /// events. + /// Same setup as , except we make the pointer path resolve to a different value. + /// We expect the ValueReacquired event to be raised first, and then the ValueChanged event. + /// + [Test] + public void ValueReacquiredAndChangedTest() + { + using var reservation = TestProcessMemory!.Reserve(16, false).Value; + TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnError(); + int targetValue = 46; + TestProcessMemory.Write(reservation.Address + 8, targetValue).ThrowOnError(); + var pointerPath = new PointerPath($"{reservation.Address:X},0"); + + var watcher = TestProcessMemory.GetAnchor(pointerPath).Watch(DefaultRefreshInterval); + bool valueReacquiredCalled = false; + bool valueChangedCalled = false; + watcher.ValueChanged += (_, args) => + { + if (valueChangedCalled) + Assert.Fail("ValueChanged event should not be raised more than once."); + + valueChangedCalled = true; + Assert.That(args.PreviousValue, Is.EqualTo(targetValue)); + Assert.That(args.NewValue, Is.EqualTo(0)); + }; + watcher.ValueReacquired += (_, args) => + { + if (valueReacquiredCalled) + Assert.Fail("ValueReacquired event should not be raised more than once."); + if (valueChangedCalled) + Assert.Fail("ValueReacquired event should be raised before ValueChanged event."); + + valueReacquiredCalled = true; + Assert.That(args.NewValue, Is.EqualTo(0)); + }; + + Assert.That(watcher.IsValueReadable, Is.True); + Assert.That(watcher.LastKnownValue, Is.EqualTo(targetValue)); + + TestProcessMemory.Write(reservation.Address, 0).ThrowOnError(); // Sabotage the pointer path + watcher.UpdateState(); // This should raise a ValueLost event + + // Make the pointer path resolve to an area with a 0 value + TestProcessMemory.Write(reservation.Address, reservation.Address + 12).ThrowOnError(); + watcher.UpdateState(); // This should raise a ValueReacquired event and then a ValueChanged event + watcher.Dispose(); + + Assert.That(watcher.IsValueReadable, Is.True); + Assert.That(valueReacquiredCalled, Is.True); + Assert.That(valueChangedCalled, Is.True); + Assert.That(watcher.LastKnownValue, Is.EqualTo(0)); + } +} + +/// +/// Runs the tests from with a 32-bit version of the target app. +/// +[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] +public class ValueWatcherTestX86 : ValueWatcherTest +{ + /// Gets a value indicating if the process is 64-bit. + protected override bool Is64Bit => false; +} \ No newline at end of file From 1b7f77cca4aee589f08b97a02a3bf9294bce6bbb Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 31 May 2025 16:18:18 +0200 Subject: [PATCH 47/66] Full documentation and tons of adjustments --- .gitignore | 4 + MindControl.sln | 58 ++++ README.md | 45 +-- doc/FindBytes.md | 123 ------- doc/GetStarted.md | 129 -------- doc/ManipulatingStrings.md | 47 --- doc/PointerPath.md | 86 ----- doc/StateWatcher.md | 49 --- doc/res/CheatEngineExamplePointer.png | Bin 16298 -> 0 bytes docs/docfx.json | 51 +++ docs/guide/guide-requirements.md | 29 ++ .../hacking-basics/basic-memory-concepts.md | 61 ++++ .../guide/hacking-basics/code-manipulation.md | 135 ++++++++ docs/guide/hacking-basics/finding-values.md | 104 ++++++ docs/guide/hacking-basics/stable-address.md | 175 ++++++++++ docs/guide/hacking-basics/unstable-address.md | 85 +++++ docs/guide/introduction.md | 26 ++ docs/guide/mcfeatures/allocations.md | 107 ++++++ docs/guide/mcfeatures/attaching.md | 75 +++++ docs/guide/mcfeatures/code-manipulation.md | 169 ++++++++++ docs/guide/mcfeatures/dll-injection.md | 45 +++ docs/guide/mcfeatures/freezing.md | 20 ++ docs/guide/mcfeatures/functions.md | 108 ++++++ docs/guide/mcfeatures/monitoring.md | 27 ++ docs/guide/mcfeatures/pointer-paths.md | 91 +++++ docs/guide/mcfeatures/reading.md | 94 ++++++ docs/guide/mcfeatures/results.md | 79 +++++ docs/guide/mcfeatures/searching.md | 52 +++ docs/guide/mcfeatures/streams.md | 26 ++ docs/guide/mcfeatures/strings.md | 180 ++++++++++ docs/guide/mcfeatures/writing.md | 85 +++++ docs/guide/project-setup/blazor-setup.md | 171 ++++++++++ docs/guide/project-setup/console-setup.md | 130 ++++++++ .../project-setup/creating-mc-project.md | 43 +++ docs/guide/project-setup/wpf-setup.md | 206 ++++++++++++ docs/guide/toc.yml | 54 +++ docs/images/ce-add-pointer.png | Bin 0 -> 14142 bytes docs/images/ce-assembly-example.png | Bin 0 -> 3353 bytes docs/images/ce-attach.png | Bin 0 -> 58235 bytes docs/images/ce-auto-assemble.png | Bin 0 -> 73435 bytes docs/images/ce-code-injection.png | Bin 0 -> 27705 bytes docs/images/ce-coinscan-firstresults.png | Bin 0 -> 9907 bytes docs/images/ce-coinscan-results-narrowed.png | Bin 0 -> 21627 bytes docs/images/ce-coinscan.png | Bin 0 -> 18704 bytes docs/images/ce-find-writes.png | Bin 0 -> 71182 bytes docs/images/ce-memoryviewer-small.png | Bin 0 -> 2381 bytes .../ce-memoryviewer-stamina-instructions.png | Bin 0 -> 36613 bytes docs/images/ce-memoryviewer.png | Bin 0 -> 71422 bytes docs/images/ce-pointer-example.png | Bin 0 -> 18045 bytes .../ce-pointer-scan-initial-results.png | Bin 0 -> 18925 bytes docs/images/ce-pointer-scan-open.png | Bin 0 -> 49969 bytes docs/images/ce-pointer-scan-options.png | Bin 0 -> 35583 bytes docs/images/ce-pointer-scan-rescan.png | Bin 0 -> 6948 bytes docs/images/ce-pointer-scan-results-2.png | Bin 0 -> 15461 bytes docs/images/ce-pointer-scan-results-final.png | Bin 0 -> 21834 bytes docs/images/ce-scan-for-pointer.png | Bin 0 -> 30687 bytes docs/images/ce-scan-stable-pointer.png | Bin 0 -> 10371 bytes docs/images/ce-stamina-find-writes.png | Bin 0 -> 18886 bytes docs/images/ce-stamina-instruction-nop.png | Bin 0 -> 28827 bytes docs/images/ce-write-trace.png | Bin 0 -> 49255 bytes docs/images/ex-blazor-app-full.png | Bin 0 -> 249792 bytes docs/images/ex-blazor-app-simple.png | Bin 0 -> 2753 bytes docs/images/ex-console-app-full.png | Bin 0 -> 127026 bytes docs/images/ex-console-app-simple.png | Bin 0 -> 29541 bytes docs/images/ex-console-app.png | Bin 0 -> 4372 bytes docs/images/ex-wpf-app-full.png | Bin 0 -> 138654 bytes docs/images/ex-wpf-app-simple.png | Bin 0 -> 2850 bytes docs/images/il-datatypes.gif | Bin 0 -> 174284 bytes docs/images/il-pointerchain.png | Bin 0 -> 17412 bytes docs/images/il-pointers-firstscenario.gif | Bin 0 -> 147149 bytes docs/images/il-pointers-secondscenario.gif | Bin 0 -> 157101 bytes docs/images/il-structures.png | Bin 0 -> 3608 bytes docs/images/sr-coincount-hacked.png | Bin 0 -> 40640 bytes docs/images/sr-coincount.png | Bin 0 -> 58646 bytes docs/index.md | 5 + docs/toc.yml | 4 + .../GameState.cs | 49 +++ ...indControl.Samples.SlimeRancherDemo.csproj | 14 + .../SlimeRancherDemo.cs | 57 ++++ .../Components/App.razor | 20 ++ .../Components/GameStateComponent.razor | 68 ++++ .../Components/Layout/MainLayout.razor | 19 ++ .../Components/Layout/MainLayout.razor.css | 96 ++++++ .../Components/Layout/NavMenu.razor | 17 + .../Components/Layout/NavMenu.razor.css | 105 ++++++ .../Components/Pages/Error.razor | 36 ++ .../Components/Pages/Home.razor | 15 + .../Components/Routes.razor | 6 + .../Components/_Imports.razor | 10 + ...MindControl.Samples.SrDemoBlazorApp.csproj | 13 + .../Program.cs | 30 ++ .../Properties/launchSettings.json | 38 +++ .../SlimeRancherDemoService.cs | 40 +++ .../appsettings.Development.json | 8 + .../appsettings.json | 9 + .../wwwroot/app.css | 71 ++++ .../wwwroot/bootstrap/bootstrap.min.css | 7 + .../wwwroot/bootstrap/bootstrap.min.css.map | 1 + .../wwwroot/favicon.png | Bin 0 -> 1148 bytes ...indControl.Samples.SrDemoConsoleApp.csproj | 14 + .../Program.cs | 107 ++++++ .../MindControl.Samples.SrDemoWpfApp/App.xaml | 9 + .../App.xaml.cs | 12 + .../AssemblyInfo.cs | 10 + .../GameStateTemplateSelector.cs | 27 ++ .../GameStateView.xaml | 58 ++++ .../GameStateView.xaml.cs | 18 + .../GameStateViewModel.cs | 88 +++++ .../MainViewModel.cs | 34 ++ .../MainWindow.xaml | 18 + .../MainWindow.xaml.cs | 15 + .../MindControl.Samples.SrDemoWpfApp.csproj | 15 + .../ViewModelBase.cs | 24 ++ src/MindControl.Code/Code/CodeChange.cs | 3 +- .../Code/ProcessMemory.Code.cs | 50 +-- .../Extensions/AssemblerExtensions.cs | 7 +- .../Hooks/ProcessMemory.Hooks.cs | 222 ++++++------- .../Results/CodeWritingFailure.cs | 123 +------ src/MindControl.Code/Results/HookFailure.cs | 159 +-------- .../Addressing/IAddressResolver.cs | 4 +- .../Addressing/LiteralAddressResolver.cs | 6 +- src/MindControl/Addressing/MemoryRange.cs | 4 +- src/MindControl/Addressing/PointerPath.cs | 81 ++++- .../Addressing/PointerPathResolver.cs | 7 +- .../Addressing/ProcessMemoryStream.cs | 4 +- .../Allocation/MemoryAllocation.cs | 8 +- .../Anchors/ByteArrayMemoryAdapter.cs | 18 +- .../Anchors/GenericMemoryAdapter.cs | 14 +- src/MindControl/Anchors/IMemoryAdapter.cs | 8 +- .../Anchors/StringPointerMemoryAdapter.cs | 15 +- src/MindControl/Anchors/ThreadValueFreezer.cs | 12 +- src/MindControl/Anchors/TimerValueFreezer.cs | 23 +- src/MindControl/Anchors/ValueAnchor.cs | 42 +-- src/MindControl/Anchors/ValueWatcher.cs | 17 +- src/MindControl/MindControl.csproj | 2 +- src/MindControl/Modules/PeParser.cs | 20 +- src/MindControl/Modules/RemoteModule.cs | 2 +- .../Native/IOperatingSystemService.cs | 26 +- src/MindControl/Native/Win32Service.cs | 150 +++++---- src/MindControl/ProcessException.cs | 36 -- .../ProcessMemory/ProcessMemory.Addressing.cs | 83 +++-- .../ProcessMemory/ProcessMemory.Allocation.cs | 88 +++-- .../ProcessMemory/ProcessMemory.Anchors.cs | 41 +-- .../ProcessMemory/ProcessMemory.FindBytes.cs | 2 +- .../ProcessMemory/ProcessMemory.Injection.cs | 30 +- .../ProcessMemory/ProcessMemory.Read.cs | 219 ++++++------- .../ProcessMemory/ProcessMemory.Threading.cs | 59 ++-- .../ProcessMemory/ProcessMemory.Write.cs | 115 +++---- .../ProcessMemory/ProcessMemory.cs | 53 +-- src/MindControl/ProcessTracker.cs | 2 +- src/MindControl/Results/AllocationFailure.cs | 39 +-- src/MindControl/Results/AttachFailure.cs | 44 +-- src/MindControl/Results/Failure.cs | 31 +- src/MindControl/Results/FindBytesFailure.cs | 7 +- .../Results/FindStringSettingsFailure.cs | 87 ----- src/MindControl/Results/InjectionFailure.cs | 57 +--- .../Results/NotSupportedFailure.cs | 13 - .../Results/PathEvaluationFailure.cs | 52 +-- src/MindControl/Results/ReadFailure.cs | 87 +---- src/MindControl/Results/ReservationFailure.cs | 30 +- src/MindControl/Results/Result.cs | 151 ++++----- .../Results/ResultFailureException.cs | 27 +- src/MindControl/Results/StoreFailure.cs | 48 --- src/MindControl/Results/StringReadFailure.cs | 96 +----- src/MindControl/Results/SystemFailure.cs | 41 +-- src/MindControl/Results/ThreadFailure.cs | 71 +--- src/MindControl/Results/WriteFailure.cs | 113 ++----- src/MindControl/Search/ByteSearchPattern.cs | 15 +- src/MindControl/Threading/RemoteThread.cs | 17 +- .../BenchmarkMemorySetup.cs | 2 +- .../Benchmarks/ReadIntByAddressBenchmark.cs | 23 ++ .../ReadLongByPointerPathBenchmark.cs | 45 +++ .../MindControl.Benchmark.csproj | 7 +- .../AddressingTests/PointerPathTest.cs | 23 ++ .../ProcessMemoryStreamTest.cs | 4 +- .../AllocationTests/MemoryAllocationTest.cs | 10 +- .../AnchorTests/ValueWatcherTest.cs | 43 ++- .../ProcessMemoryCodeExtensionsTest.cs | 28 +- .../ProcessMemoryHookExtensionsTest.cs | 112 +++---- .../ProcessMemoryAllocationTest.cs | 50 +-- .../ProcessMemoryAnchorTest.cs | 37 ++- .../ProcessMemoryAttachTest.cs | 8 +- .../ProcessMemoryEvaluateTest.cs | 67 ++-- .../ProcessMemoryInjectionTest.cs | 21 +- .../ProcessMemoryReadTest.cs | 310 ++++++++---------- .../ProcessMemoryThreadingTest.cs | 62 ++-- .../ProcessMemoryWriteTest.cs | 38 ++- .../ProcessMemoryTests/RemoteModuleTest.cs | 2 +- .../SearchTests/ByteSearchPatternTest.cs | 2 +- 189 files changed, 5122 insertions(+), 2709 deletions(-) delete mode 100644 doc/FindBytes.md delete mode 100644 doc/GetStarted.md delete mode 100644 doc/ManipulatingStrings.md delete mode 100644 doc/PointerPath.md delete mode 100644 doc/StateWatcher.md delete mode 100644 doc/res/CheatEngineExamplePointer.png create mode 100644 docs/docfx.json create mode 100644 docs/guide/guide-requirements.md create mode 100644 docs/guide/hacking-basics/basic-memory-concepts.md create mode 100644 docs/guide/hacking-basics/code-manipulation.md create mode 100644 docs/guide/hacking-basics/finding-values.md create mode 100644 docs/guide/hacking-basics/stable-address.md create mode 100644 docs/guide/hacking-basics/unstable-address.md create mode 100644 docs/guide/introduction.md create mode 100644 docs/guide/mcfeatures/allocations.md create mode 100644 docs/guide/mcfeatures/attaching.md create mode 100644 docs/guide/mcfeatures/code-manipulation.md create mode 100644 docs/guide/mcfeatures/dll-injection.md create mode 100644 docs/guide/mcfeatures/freezing.md create mode 100644 docs/guide/mcfeatures/functions.md create mode 100644 docs/guide/mcfeatures/monitoring.md create mode 100644 docs/guide/mcfeatures/pointer-paths.md create mode 100644 docs/guide/mcfeatures/reading.md create mode 100644 docs/guide/mcfeatures/results.md create mode 100644 docs/guide/mcfeatures/searching.md create mode 100644 docs/guide/mcfeatures/streams.md create mode 100644 docs/guide/mcfeatures/strings.md create mode 100644 docs/guide/mcfeatures/writing.md create mode 100644 docs/guide/project-setup/blazor-setup.md create mode 100644 docs/guide/project-setup/console-setup.md create mode 100644 docs/guide/project-setup/creating-mc-project.md create mode 100644 docs/guide/project-setup/wpf-setup.md create mode 100644 docs/guide/toc.yml create mode 100644 docs/images/ce-add-pointer.png create mode 100644 docs/images/ce-assembly-example.png create mode 100644 docs/images/ce-attach.png create mode 100644 docs/images/ce-auto-assemble.png create mode 100644 docs/images/ce-code-injection.png create mode 100644 docs/images/ce-coinscan-firstresults.png create mode 100644 docs/images/ce-coinscan-results-narrowed.png create mode 100644 docs/images/ce-coinscan.png create mode 100644 docs/images/ce-find-writes.png create mode 100644 docs/images/ce-memoryviewer-small.png create mode 100644 docs/images/ce-memoryviewer-stamina-instructions.png create mode 100644 docs/images/ce-memoryviewer.png create mode 100644 docs/images/ce-pointer-example.png create mode 100644 docs/images/ce-pointer-scan-initial-results.png create mode 100644 docs/images/ce-pointer-scan-open.png create mode 100644 docs/images/ce-pointer-scan-options.png create mode 100644 docs/images/ce-pointer-scan-rescan.png create mode 100644 docs/images/ce-pointer-scan-results-2.png create mode 100644 docs/images/ce-pointer-scan-results-final.png create mode 100644 docs/images/ce-scan-for-pointer.png create mode 100644 docs/images/ce-scan-stable-pointer.png create mode 100644 docs/images/ce-stamina-find-writes.png create mode 100644 docs/images/ce-stamina-instruction-nop.png create mode 100644 docs/images/ce-write-trace.png create mode 100644 docs/images/ex-blazor-app-full.png create mode 100644 docs/images/ex-blazor-app-simple.png create mode 100644 docs/images/ex-console-app-full.png create mode 100644 docs/images/ex-console-app-simple.png create mode 100644 docs/images/ex-console-app.png create mode 100644 docs/images/ex-wpf-app-full.png create mode 100644 docs/images/ex-wpf-app-simple.png create mode 100644 docs/images/il-datatypes.gif create mode 100644 docs/images/il-pointerchain.png create mode 100644 docs/images/il-pointers-firstscenario.gif create mode 100644 docs/images/il-pointers-secondscenario.gif create mode 100644 docs/images/il-structures.png create mode 100644 docs/images/sr-coincount-hacked.png create mode 100644 docs/images/sr-coincount.png create mode 100644 docs/index.md create mode 100644 docs/toc.yml create mode 100644 samples/MindControl.Samples.SlimeRancherDemo/GameState.cs create mode 100644 samples/MindControl.Samples.SlimeRancherDemo/MindControl.Samples.SlimeRancherDemo.csproj create mode 100644 samples/MindControl.Samples.SlimeRancherDemo/SlimeRancherDemo.cs create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/Components/App.razor create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/Components/Components/GameStateComponent.razor create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/MainLayout.razor create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/MainLayout.razor.css create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/NavMenu.razor create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/NavMenu.razor.css create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/Components/Pages/Error.razor create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/Components/Pages/Home.razor create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/Components/Routes.razor create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/Components/_Imports.razor create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/MindControl.Samples.SrDemoBlazorApp.csproj create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/Program.cs create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/Properties/launchSettings.json create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/SlimeRancherDemoService.cs create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/appsettings.Development.json create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/appsettings.json create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/app.css create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/bootstrap/bootstrap.min.css create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/bootstrap/bootstrap.min.css.map create mode 100644 samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/favicon.png create mode 100644 samples/MindControl.Samples.SrDemoConsoleApp/MindControl.Samples.SrDemoConsoleApp.csproj create mode 100644 samples/MindControl.Samples.SrDemoConsoleApp/Program.cs create mode 100644 samples/MindControl.Samples.SrDemoWpfApp/App.xaml create mode 100644 samples/MindControl.Samples.SrDemoWpfApp/App.xaml.cs create mode 100644 samples/MindControl.Samples.SrDemoWpfApp/AssemblyInfo.cs create mode 100644 samples/MindControl.Samples.SrDemoWpfApp/GameStateTemplateSelector.cs create mode 100644 samples/MindControl.Samples.SrDemoWpfApp/GameStateView.xaml create mode 100644 samples/MindControl.Samples.SrDemoWpfApp/GameStateView.xaml.cs create mode 100644 samples/MindControl.Samples.SrDemoWpfApp/GameStateViewModel.cs create mode 100644 samples/MindControl.Samples.SrDemoWpfApp/MainViewModel.cs create mode 100644 samples/MindControl.Samples.SrDemoWpfApp/MainWindow.xaml create mode 100644 samples/MindControl.Samples.SrDemoWpfApp/MainWindow.xaml.cs create mode 100644 samples/MindControl.Samples.SrDemoWpfApp/MindControl.Samples.SrDemoWpfApp.csproj create mode 100644 samples/MindControl.Samples.SrDemoWpfApp/ViewModelBase.cs delete mode 100644 src/MindControl/ProcessException.cs delete mode 100644 src/MindControl/Results/FindStringSettingsFailure.cs delete mode 100644 src/MindControl/Results/NotSupportedFailure.cs delete mode 100644 src/MindControl/Results/StoreFailure.cs diff --git a/.gitignore b/.gitignore index 888bf30..4c61720 100644 --- a/.gitignore +++ b/.gitignore @@ -365,3 +365,7 @@ MigrationBackup/ FodyWeavers.xsd /CommandCenter.App/.config /CommandCenter.App/Plugins/ + +# docfx +docs/_site/ +docs/api/ \ No newline at end of file diff --git a/MindControl.sln b/MindControl.sln index 3b2842e..25e89f7 100644 --- a/MindControl.sln +++ b/MindControl.sln @@ -15,6 +15,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindControl.Benchmark", "te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindControl.Code", "src\MindControl.Code\MindControl.Code.csproj", "{BF166555-9220-42C6-A93E-EC9FBA9AC38C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{751C861A-AA2D-454F-8910-E27040CDEF6B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "libraries", "libraries", "{1D218910-68CD-4D94-B928-A0ADE5A600D5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{0923ADDB-5517-4ABF-A38E-CC3776C85A71}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindControl.Samples.SrDemoConsoleApp", "samples\MindControl.Samples.SrDemoConsoleApp\MindControl.Samples.SrDemoConsoleApp.csproj", "{B7E8F1B0-F5D2-4943-B780-CB1C0C248824}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindControl.Samples.SrDemoWpfApp", "samples\MindControl.Samples.SrDemoWpfApp\MindControl.Samples.SrDemoWpfApp.csproj", "{09A4F714-3F5A-4CD8-94D9-902BBDE5A090}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindControl.Samples.SlimeRancherDemo", "samples\MindControl.Samples.SlimeRancherDemo\MindControl.Samples.SlimeRancherDemo.csproj", "{8E00D032-5E48-4DA3-B2BF-C3DC3C57EA35}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindControl.Samples.SrDemoBlazorApp", "samples\MindControl.Samples.SrDemoBlazorApp\MindControl.Samples.SrDemoBlazorApp.csproj", "{125FFA82-F622-4981-8D9F-C5FB7CE87D56}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -56,8 +70,52 @@ Global {BF166555-9220-42C6-A93E-EC9FBA9AC38C}.Release|Any CPU.ActiveCfg = Release|Any CPU {BF166555-9220-42C6-A93E-EC9FBA9AC38C}.Release|Any CPU.Build.0 = Release|Any CPU {BF166555-9220-42C6-A93E-EC9FBA9AC38C}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7E8F1B0-F5D2-4943-B780-CB1C0C248824}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7E8F1B0-F5D2-4943-B780-CB1C0C248824}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7E8F1B0-F5D2-4943-B780-CB1C0C248824}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7E8F1B0-F5D2-4943-B780-CB1C0C248824}.Release|Any CPU.Build.0 = Release|Any CPU + {B7E8F1B0-F5D2-4943-B780-CB1C0C248824}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7E8F1B0-F5D2-4943-B780-CB1C0C248824}.Debug|x86.Build.0 = Debug|Any CPU + {B7E8F1B0-F5D2-4943-B780-CB1C0C248824}.Release|x86.ActiveCfg = Release|Any CPU + {B7E8F1B0-F5D2-4943-B780-CB1C0C248824}.Release|x86.Build.0 = Release|Any CPU + {09A4F714-3F5A-4CD8-94D9-902BBDE5A090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09A4F714-3F5A-4CD8-94D9-902BBDE5A090}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09A4F714-3F5A-4CD8-94D9-902BBDE5A090}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09A4F714-3F5A-4CD8-94D9-902BBDE5A090}.Release|Any CPU.Build.0 = Release|Any CPU + {09A4F714-3F5A-4CD8-94D9-902BBDE5A090}.Debug|x86.ActiveCfg = Debug|Any CPU + {09A4F714-3F5A-4CD8-94D9-902BBDE5A090}.Debug|x86.Build.0 = Debug|Any CPU + {09A4F714-3F5A-4CD8-94D9-902BBDE5A090}.Release|x86.ActiveCfg = Release|Any CPU + {09A4F714-3F5A-4CD8-94D9-902BBDE5A090}.Release|x86.Build.0 = Release|Any CPU + {8E00D032-5E48-4DA3-B2BF-C3DC3C57EA35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E00D032-5E48-4DA3-B2BF-C3DC3C57EA35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E00D032-5E48-4DA3-B2BF-C3DC3C57EA35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E00D032-5E48-4DA3-B2BF-C3DC3C57EA35}.Release|Any CPU.Build.0 = Release|Any CPU + {8E00D032-5E48-4DA3-B2BF-C3DC3C57EA35}.Debug|x86.ActiveCfg = Debug|Any CPU + {8E00D032-5E48-4DA3-B2BF-C3DC3C57EA35}.Debug|x86.Build.0 = Debug|Any CPU + {8E00D032-5E48-4DA3-B2BF-C3DC3C57EA35}.Release|x86.ActiveCfg = Release|Any CPU + {8E00D032-5E48-4DA3-B2BF-C3DC3C57EA35}.Release|x86.Build.0 = Release|Any CPU + {125FFA82-F622-4981-8D9F-C5FB7CE87D56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {125FFA82-F622-4981-8D9F-C5FB7CE87D56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {125FFA82-F622-4981-8D9F-C5FB7CE87D56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {125FFA82-F622-4981-8D9F-C5FB7CE87D56}.Release|Any CPU.Build.0 = Release|Any CPU + {125FFA82-F622-4981-8D9F-C5FB7CE87D56}.Debug|x86.ActiveCfg = Debug|Any CPU + {125FFA82-F622-4981-8D9F-C5FB7CE87D56}.Debug|x86.Build.0 = Debug|Any CPU + {125FFA82-F622-4981-8D9F-C5FB7CE87D56}.Release|x86.ActiveCfg = Release|Any CPU + {125FFA82-F622-4981-8D9F-C5FB7CE87D56}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {5C1B693D-D176-41B2-A47A-E78E098171CB} = {751C861A-AA2D-454F-8910-E27040CDEF6B} + {2BE5C902-70EC-4DBE-B782-FB0623518F96} = {751C861A-AA2D-454F-8910-E27040CDEF6B} + {AA8C9AF6-7C31-43D1-A519-DBB1C3252127} = {751C861A-AA2D-454F-8910-E27040CDEF6B} + {9FE5EF8E-1230-42DB-A6B2-0C2633D32A1F} = {751C861A-AA2D-454F-8910-E27040CDEF6B} + {A13C976F-5866-48C4-904E-2C1960220FAC} = {1D218910-68CD-4D94-B928-A0ADE5A600D5} + {BF166555-9220-42C6-A93E-EC9FBA9AC38C} = {1D218910-68CD-4D94-B928-A0ADE5A600D5} + {B7E8F1B0-F5D2-4943-B780-CB1C0C248824} = {0923ADDB-5517-4ABF-A38E-CC3776C85A71} + {09A4F714-3F5A-4CD8-94D9-902BBDE5A090} = {0923ADDB-5517-4ABF-A38E-CC3776C85A71} + {8E00D032-5E48-4DA3-B2BF-C3DC3C57EA35} = {0923ADDB-5517-4ABF-A38E-CC3776C85A71} + {125FFA82-F622-4981-8D9F-C5FB7CE87D56} = {0923ADDB-5517-4ABF-A38E-CC3776C85A71} + EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 8617fbb..e16c5d8 100644 --- a/README.md +++ b/README.md @@ -9,58 +9,39 @@ MindControl is a .net hacking library for Windows that allows you to manipulate Here is a quick example to get you started. ```csharp -var myGame = ProcessMemory.OpenProcess("mygame.exe"); // A process with this name must be running +var myGame = ProcessMemory.OpenProcess("mygame").Result; // A process with this name must be running var hpAddress = new PointerPath("mygame.exe+1D005A70,1C,8"); // See the docs for how to determine these // Read values -int currentHp = myGame.ReadInt(hpAddress); +var currentHp = myGame.Read(hpAddress); Console.WriteLine($"You have {currentHp}HP"); // Example output: "You have 50HP" // Write values -myGame.WriteInt(hpAddress, 9999); +myGame.Write(hpAddress, 9999); // Find the first occurrence of a pattern in memory, with wildcard bytes UIntPtr targetAddress = myGame.FindBytes("4D 79 ?? ?? ?? ?? ?? ?? 56 61 6C 75 65") .FirstOrDefault(); + +// ... And many more features ``` -See [the documentation](doc/GetStarted.md) for more information. +See [the documentation](doc/GetStarted.md) to get started, whether you already dabble in memory hacking or are completely new to it. ## Features - Attach to any process easily by name or PID - Address memory either through simple pointer addresses, or through dynamic pointer paths (e.g. `mygame.exe+1D005A70,1C,8`) -- Read a memory address as a byte array, boolean, or any basic number types -- Read a memory address as a string as simply as possible, or as complex as you need -- Write byte arrays, booleans and basic number types at any memory address +- Read and write byte arrays, booleans, numbers of any kind, strings and structures - Inject DLLs to execute arbitrary code in the target process -- Search for byte patterns in the target process memory +- Search for byte sequences or patterns in the target process memory +- Manage memory allocations manually or automatically, to store data or code in the target process +- Insert assembly code with hooks +- Replace or remove existing code +- Start threads in the target process - Designed for performance and simplicity of use - Unit tested and made with care ## Comparison with other libraries -MindControl is a small library that focuses on the most common use cases for hacking. - -It is not as feature-rich as the most used .net hacking library, [memory.dll](https://github.com/erfg12/memory.dll/), but it aims to be easier to use, have comparable performance, and most importantly to be more reliable and maintainable. - -If you are considering MindControl but unsure if it has the features you need, here is a comparison table. - -| Feature | MindControl | memory.dll -|-----------------------------|--- |--- | -| **Handle pointer paths** | ✔️ | ✔️ | -| **Read primitive types** | ✔️ | ✔️ | -| **Write primitive types** | ✔️ | ✔️ | -| **Read strings** | ✔️ | ✔️ | -| **Write strings** | ❌ | ✔️ | -| **Array of bytes scanning** | ✔️ | ✔️ | -| **Inject DLLs** | ✔️ | ✔️ | -| **State watchers** | ✔️ | ❌ | -| **Create code caves** | ❌ | ✔️ | -| **Bind to UI elements** | ❌ | ✔️ | -| **Freeze values** | ❌ | ✔️ | -| **Set focus to process** | ❌ | ✔️ | -| **Load from .ini file** | ❌ | ✔️ | -| **Suspend process** | ❌ | ✔️ | -| **Dump process memory** | ❌ | ✔️ | -| **Manipulate threads** | ❌ | ✔️ | +MindControl has a lot in common with other .net hacking libraries, the most popular one being [memory.dll](https://github.com/erfg12/memory.dll/). While the latter focuses on practicality and has some bias towards a specific use-case (game trainers), MindControl primarily aims to be more generic, reliable and maintainable, generally easier to understand and to use, and with similar or better performance. diff --git a/doc/FindBytes.md b/doc/FindBytes.md deleted file mode 100644 index 110168d..0000000 --- a/doc/FindBytes.md +++ /dev/null @@ -1,123 +0,0 @@ -# Finding patterns in memory with FindBytes - -MindControl allows you to search for byte patterns in the memory of your target process. This can be useful to find the location of a value you want to hack, or to find a function you want to hook. - -```csharp -var myGame = ProcessMemory.OpenProcess("mygame"); -UIntPtr targetAddress = myGame.FindBytes("4D 79 ?? ?? ?? ?? ?? ?? 56 61 6C 75 65") - .FirstOrDefault(); -``` - -This is also called "AoB scanning" (where AoB stands for "Array of bytes"). - -## Pattern syntax - -The pattern you provide to `FindBytes` is a string of hexadecimal bytes, optionally separated by spaces. Each byte can be a specific value (e.g. `4D`), or a wildcard (e.g. `??`). The wildcard will match any byte. - -You can also use partial wildcards, for example `4?` or `?D` to match any byte that starts with `4` or ends with `D`. - -For better performances, avoid starting your pattern with a wildcard. - -## Search range - -By default, `FindBytes` will search the entire memory of the target process. This can be slow, especially if the process has a lot of memory. - -You can limit the search to a specific range of memory by using the `range` optional parameter. - -The example below demonstrates how to search only in the range of a specific module. - -```csharp -var myGame = ProcessMemory.OpenProcess("mygame"); - -// Get the range for the module that contains the game executable -// Instead of "mygame.exe", you can use the name of any module loaded in the process (usually a DLL) -var myModuleRange = myGame.Modules.FirstOrDefault(m => m.Name == "mygame.exe"); -List results = myGame.FindBytes("4D 79 ?? ?? 65", range: myModuleRange) - .ToList(); -``` - -You can also specify a custom range by creating a `MemoryRange` instance: - -```csharp - // Both of the following ranges can be used to search from 0x5000 to 0x5FFF included -var rangeWithStartAndEnd = new MemoryRange(0x5000, 0x5FFF); -var rangeWithStartAndSize = MemoryRange.FromStartAndSize(0x5000, 0x1000); -``` - -To optimize the performance of your search, you should always specify a range. Usually, you can figure out at least the module that contains the value you are looking for, but the more specific you can be, the faster the search will be. - -## Search parameters - -The `FindBytes` method takes an optional `FindBytesSetting` parameter that allows you to specify what kind of memory you want to search, and limit the number of results. - -Depending on what kind of data you are looking for, you can specify constraints on the kind of memory you want to search. For example, you can search only executable memory, or only memory that is writable. - -Here is an example of how to search for a pattern in the executable code of your target process: - -```csharp -var searchSettings = new FindBytesSettings -{ - SearchExecutable = true, - SearchWritable = false -}; -List codeResults = myGame.FindBytes("4D 79 ?? ?? 65", settings: searchSettings) - .ToList(); -``` - -Specifying these settings has multiple benefits: -- It will make the search faster, as it will skip memory that is not relevant to your search. -- It will make the search more reliable, as it will avoid returning results that are not useful to you. - -## Using the results - -The `FindBytes` method returns an `IEnumerable` that contains the addresses of the memory locations where the pattern was found. You can then use these addresses to read or write memory, or to hook functions. - -Here is an example of how to read a value at the first address found, with an offset: - -```csharp -var myGame = ProcessMemory.OpenProcess("mygame"); -UIntPtr targetAddress = myGame.FindBytes("4D 79 28 2A ?? ?? ?? ?? 75 65") - .FirstOrDefault(); - -// The line below reads the actual value of the wildcard part of the pattern, skipping the first 4 bytes -int value = myGame.ReadInt(targetAddress + 0x4); -``` - -When using the results of `FindBytes`, be careful of multiple enumerations. If you need to use the results multiple times, consider storing them in a list: - -```csharp -var results = myGame.FindBytes("4D 79 ?? ?? 65").ToList(); -``` - -If you only need the first result, you can use `FirstOrDefault` as shown in the examples above. This will ensure that the search stops as soon as a match is found, which can speed up the execution dramatically, depending on your use case. - -## Preventing hangs - -If you are searching for a pattern in a large memory range, the search can take a long time. - -If you are running this code in a UI application, your application might become unresponsive during the search. - -To prevent this, you should use the asynchronous variant `FindByesAsync`, which returns an `IAsyncEnumerable`. - -Here is an example of how to prevent your application from hanging during the search: - -```csharp -private async Task OnButtonClick() -{ - var results = myGame.FindBytesAsync("4D 79 ?? ?? 65"); - await foreach (UIntPtr result in results) - { - // Do something with the result - } -} -``` - -If you want to only get the first result, or work with an array or a list of results, you can use the `FirstOrDefaultAsync`, `ToArrayAsync`, or `ToListAsync` extension methods from the `System.Linq.Async` package. - -```csharp -private async Task OnButtonClick() -{ - UIntPtr result = await myGame.FindBytesAsync("4D 79 ?? ?? 65").FirstOrDefaultAsync(); - // Do something with the result -} -``` diff --git a/doc/GetStarted.md b/doc/GetStarted.md deleted file mode 100644 index 37b2dd8..0000000 --- a/doc/GetStarted.md +++ /dev/null @@ -1,129 +0,0 @@ -# Get started using MindControl - -> **DO NOT use this library to cheat in online competitive games. Cheaters ruins the fun for everyone. If you ignore this warning, I will do my best to shut down your project.** - -This documentation aims to provide you with basic directions on how to get started hacking a process with the MindControl library. - -## Basic principle - -Let's go through the basics of hacking a health value in an imaginary game. - -```csharp -// Open a process named "mygame" that is currently running -var myGame = ProcessMemory.OpenProcess("mygame"); - -// Build a path that points to the health value or anything else you want to hack -// To determine these, use tools like Cheat Engine (keep reading the docs for more info) -var hpAddress = new PointerPath("mygame.exe+1D005A70,1C,8"); - -// Read the health value. In this example, it's an integer, but it can be any basic type. -int currentHp = myGame.ReadInt(hpAddress); -Console.WriteLine($"You have {currentHp}HP"); // Example output: "You have 50HP" - -// Overwrite the health value of the game so that our health goes to 9999. -myGame.WriteInt(hpAddress, 9999); -``` - -This code should give you an idea of how the library works and how to build basic hacks. - -## Figuring out pointer paths - -The key in the example shown above is the pointer path, that allows us to reference the address of the health value in the internal memory of our imaginary game. - -The pointer path to use will be different for each case, and also need to be stable in order to work every time. To figure out those stable pointers, you will need to experiment with tools such as [Cheat Engine](https://www.cheatengine.org), or use pointers that others have figured out through this process. - -This documentation will not go through the process of finding stable pointer paths. Cheat Engine has tutorials, and there are lots of video resources out there that you can check out. - -Once you've figured out your stable pointer, just transcribe it into the PointerPath expression syntax and you're ready to hack! Here's a quick example of transcribing a pointer from Cheat Engine: - -![An example Cheat Engine pointer](res/CheatEngineExamplePointer.png) - -Just go from the bottom up, and separate every field with a `,`. - -The result: `"UnityPlayer.dll"+017CD668,1F0,1E8,4E0,DB8,B8,D0,8,68`. - -Stable pointers usually start with a module name, in this case `"UnityPlayer.dll"`, but you don't have to use one. Check out the [pointer path doc](PointerPath.md) if you need more info. - -## Gotchas and common pitfalls - -Hacking is hard and comes with a lot of surprises and hurdles. Take your time, make sure you understand what you're doing, and you'll get there eventually. - -Here are common reasons for your programs to fail: -- Your hacking program may need to be run as an administrator. This can be required to manipulate memory, disable protections, and other internal operations that MindControl does. -- You are getting/setting wrong values because your pointer path is not stable. Double-check with Cheat Engine that your pointer does work in the same circumstances. -- You cannot read string properly. Check out the [Manipulating strings](ManipulatingStrings.md) documentation. - -## Finding patterns in memory - -MindControl allows you to search for byte patterns in the memory of your target process. This can be useful to find the location of a value you want to hack, or to find a function you want to hook. - -```csharp -var myGame = ProcessMemory.OpenProcess("mygame"); -UIntPtr targetAddress = myGame.FindBytes("4D 79 ?? ?? ?? ?? ?? ?? 56 61 6C 75 65") - .FirstOrDefault(); -``` - -Find out more about this on the [FindBytes](FindBytes.md) documentation. - -## Handle process exit and restart - -The `ProcessMemory` class has a `ProcessDetached` event that you can use to react to your target process exiting or crashing. Note that it will also fire when disposing the instance. - -```csharp -var myGame = ProcessMemory.OpenProcess("mygame"); -myGame.ProcessDetached += (_, _) => { Console.WriteLine("Target process is detached."); } -``` - -However, a `ProcessMemory` instance that has been detached **cannot be reattached**. - -If you want to handle your target process exiting and potentially restarting (or starting after your hacking program), use the `ProcessTracker` class. - -```csharp -var tracker = new ProcessTracker("mygame"); -tracker.Attached += (_, _) => { Console.WriteLine("Target is attached."); } -tracker.Detached += (_, _) => { Console.WriteLine("Target is detached."); } - -var myGame = tracker.GetProcessMemory(); -``` - -The `GetProcessMemory` method will return an attached `ProcessMemory` instance, or null if the target process is not running. It will automatically handle target process restarts by creating a new `ProcessMemory` instance when the existing one has been detached. - -Just make sure to use the freshest instance from the tracker in your reading/writing methods, and not a stored `ProcessMemory` variable: - -```csharp -public MyHackingClass() -{ - Tracker = new ProcessTracker("mygame"); -} -public void Update() - => DoSomeHacking(Tracker.GetProcessMemory()); // ✓ This will handle restarts -``` - -VS - -```csharp -public MyHackingClass() -{ - var tracker = new ProcessTracker("mygame"); - MyGame = tracker.GetProcessMemory(); -} -public void Update() - => DoSomeHacking(MyGame); // ✖ This will NOT handle restarts. -``` - -## Inject libraries - -MindControl allows you to inject DLLs into the target process, which can be used to execute arbitrary code in the target process. - -```csharp -var myGame = ProcessMemory.OpenProcess("mygame"); -myGame.InjectLibrary("myhack.dll"); -``` - -For example, you can use named pipes to communicate between your hacking program and the target process (send commands to the game or receive data from it). - -## Tracking a process in real-time - -The `StateWatcher` class gives you a convenient way to access the internal data of your target process, with automatic refreshes. - -Check the [StateWatcher](StateWatcher.md) documentation to learn how to set it up. diff --git a/doc/ManipulatingStrings.md b/doc/ManipulatingStrings.md deleted file mode 100644 index 364f74c..0000000 --- a/doc/ManipulatingStrings.md +++ /dev/null @@ -1,47 +0,0 @@ -# Manipulating strings - -Strings are more complicated than the basic boolean and numeric types: they usually are object instances with their own memory allocation, they have a length, an encoding, and there is no globally consistent way to handle them. - -To take a couple of examples, programs using the .net framework will have strings stored with a 4-byte prefix indicating their length, and use the UTF-16 encoding and a \0 terminator, while programs made with Rust will likely use UTF-8 with no terminator and a 2-byte length prefix. - -## Reading strings - -In addition to the usual parameters, the `ReadString` method takes a max length and a `StringSettings` instance parameters. - -## Using the right StringSettings - -Once you have located the string you want to read in memory, you might find that calling the `ReadString` method with the default parameters works just as you would expect. - -If the result doesn't look right, read through the "Problems and solutions" section of this guide, experiment with various presets (e.g. `StringSettings.DefaultUtf8`) or try to figure out exactly what settings to use. - -To build your own `StringSettings` instance, just use the constructor and supply the following parameters: -- `encoding`: this should almost always be either `System.Text.Encoding.UTF8` or `System.Text.Encoding.Unicode` (which is UTF-16). You will recognize the latter when inspecting your target string if each character seems to take two bytes (usually, the second byte will be `00`). -- `isNullTerminated`: this specifies whether your strings are terminated with a `\0` character (the value 0, which is also called `null`). Some languages or frameworks read strings character-by-character until they encounter this terminator. Others rely on a length prefix. And sometimes you will have both. You can usually set this to `true` if you have a `00` at the end of your string, but it's usually better to set it to `false` if you have a length prefix (see the following parameter). -- `prefixSettings`: this is an instance of `StringLengthPrefixSettings`, that specifies if your string is length-prefixed, and if so, how to read that prefix. A length prefix is a number that comes right before the first character of your string and that is equal to the number of characters or bytes in your string. If your string doesn't seem to have one, set this parameter to `null`. Otherwise, you can build an instance with the given parameters: - - `prefixSize`: set this to the number of bytes of the length prefix. It will usually be either `2` or `4`. Check how long the prefix seems to be on your string with a memory inspector. - - `lengthUnit`: when reading a string, the number in the prefix will be multiplied by this parameter to obtain the number of bytes to read. So if the length prefix is the number of bytes, specify `1`. If the prefix reads 5 but your string is 10-bytes-long, specify `2`. If you omit this parameter or set it to `null`, it will be determined automatically. - -**Here are a few additional considerations:** -- The `maxSizeInBytes` parameter in `ReadString` is always applied, no matter what `StringSettings` you specify. It also has a default value, so be aware of that if you need to read a string that might be long. -- If your `StringSettings` specify a length prefix, your pointer path needs to point to the start of the prefix, not the first character of the string. If you ignore this, you will be getting results where the first characters are missing and the string length might not be right. - -### Problems and solutions - -- **The result I get from `ReadString` looks like a bunch of garbled, random characters** - - You are probably not using the right encoding. Try using `StringSettings.Default` and then `StringSettings.DefaultUtf8`. One of the two should either work, or leave you with more minor issues. - - If the solution above does not work, check your pointer path. - - -- **The result I get from `ReadString` is missing a few characters at the start** - - If your string has a length prefix, make sure that your pointer path points at the start of the length prefix, not at the first character of the string. Typically, your pointer path will end with `,C` when it should end with `,8`. - - If your string does not have a length prefix, make sure you specify a `StringSettings` instance with a null `PrefixSettings`. - - -- **The result I get from `ReadString` is longer than expected** - - Check if your string is length-prefixed, and make sure to use appropriate `StringSettings`, with a matching `StringLengthPrefixSettings` instance. - - If your string isn't length-prefixed, use a `StringSettings` that has `IsNullTerminated` set to `true`. - - -- **The result I get from `ReadString` cuts at about half the length** - - If you are using a `StringSettings` instance with a length prefix specification, try setting the `LengthUnit` of the `StringLengthPrefixSettings` instance to `2`. - - Check the `maxLength` parameter that you supply to `ReadString` (and remember that its value is in bytes, not characters). \ No newline at end of file diff --git a/doc/PointerPath.md b/doc/PointerPath.md deleted file mode 100644 index 51349b6..0000000 --- a/doc/PointerPath.md +++ /dev/null @@ -1,86 +0,0 @@ -# PointerPath expressions - -This guide will give you a comprehensive look at how to use the `PointerPath` class and its expressions. - -The goal of a pointer path is to detail a path in memory that you can follow to get to the address of a targeted value, by reading a series of pointers. They are used as an alternative to static addresses, that are usually not stable (meaning they do not always point to the right value in memory). - -Read the "Quick look" section below to learn about pretty much everything you need to know. There is usually no need to go further than that in most scenarios. - -## Quick look - -Here is an example of a PointerPath expression: - -`"mygame.exe"+1D5A10,1C,8` - -Let's decompose it: -- `"mygame.exe"` - -This is a module name. Most of the time, you should work with module names to get stable pointers. Modules are files loaded in memory at a particular address. This gives us a base address to start with. - -- `+1D5A10` - -This is the base module offset. It applies to the address of the module. In this case, our first pointer is at the address of the `"mygame.exe"` module, plus 1923600 (1D5A10) bytes. - -- `,1C` - -This is the offset to the second pointer in the path. You can tell by the `,` which separates each pointer to follow. In this case, the second pointer is 28 (1C) bytes after following the first one. - -- `,8` - -This is the offset to the third and final pointer in the path. The last pointer is 8 bytes after following the second one. - -### Evaluating the pointer path - -Evaluating a pointer path is the process of translating it to a static memory address, that we can read from or write to. Pointer paths are evaluated every time they are used. - -When evaluating our example pointer path, this is what happens: -1. The address of the `"mygame.exe"` module is determined through the process' module list -2. The first offset `1D5A10` is added to the address of the module -3. A pointer is read from the memory at the resulting address. -4. The second offset `1C` is added to the pointer read at the previous step. -5. A pointer is read from the memory at the resulting address. -6. The third offset `8` is added to the pointer read at the previous step. - -This gives us our final address that we can read from or write to. - -## Full syntax guide - -This section is here for a more comprehensive syntax breakdown. You should be able to handle most cases with the information in the previous section, but keep reading if you want to learn more advanced use cases. - -### Structure - -A PointerPath expression is basically comprised of a starting address, and a series of offsets. Each part is separated with a `,`. - -#### Starting address syntax - -An expression will always start with a starting address, that provides a first address to start with. It cannot contain any additional starting addresses. - -The starting address uses the same syntax as an offset, except it can also start with a module name. - -Typically, a module name looks like `"mygame.exe"`, but here are some other valid module names: `"mygame.data"`, `mygame.exe`, `mygame`, `my game`, `"1F8D"`. - -This highlights a few things: -- A module name does not necessarily start and end with `"` (unless it's otherwise a valid hexadecimal number as in that last example). These `"` characters are trimmed during evaluation to find out the actual module name. -- The module name may have any extension, or no extension at all. -- The module name may contain spaces. - -In addition to the module name, a starting address may, like any offset, have any number of static offsets chained together with `+` or `-` operators. Here are some valid examples of a starting address: `"mygame.exe"+0F`, `mygame.exe-0F`, `mygame+1C-4+2`. See the "Offset syntax" section below for more info. - -It shall be noted that a starting address does not necessarily start with a module name. It may also be any static address. Here is a couple of examples of full, valid PointerPath expressions with no module name: `1F016644,13,A0`, `1F016644+13,A0`. - -#### Offset syntax - -Any number of offset expressions can follow the starting address expression. - -An offset expression is comprised of at least one hexadecimal number. It can be added together with others, through a series of algorithmic `+` or `-` operators. - -Additionally, an offset expression can start with a `-` sign to indicate a negative offset. - -Here are a few examples of valid offsets and what they evaluate to: -- `2A` evaluates to `2A` -- `2A+4` evaluates to `2E` -- `2A-3` evaluates to `27` -- `2A+4-4+2` evaluates to `2C` -- `-2A` evaluates to `-2A` - -Two operators cannot be chained, and an offset cannot end with an operator. \ No newline at end of file diff --git a/doc/StateWatcher.md b/doc/StateWatcher.md deleted file mode 100644 index ea4e717..0000000 --- a/doc/StateWatcher.md +++ /dev/null @@ -1,49 +0,0 @@ -# StateWatcher - -The `StateWatcher` abstract class allows you to easily track values in real-time. This is a great fit if you're trying to display up-to-date values from a game on a user interface like a WPF or Blazor application for example. - -The way it works is that it periodically updates its `LatestState` property automatically, which works well in data binding scenarios, but also has events that you can subscribe to, in order to directly receive each and every state update if you need to. - -To use it, you have define a class that inherits it, and implement the `ReadState` method. - -Here is an example: - -```csharp -public record MyGameState(int HealthPoints, string PlayerName); - -public class MyGameWatcher : StateWatcher -{ - private ProcessMemory _myGame; - - public MyGameWatcher() - : base(30) // This makes it refresh 30 times per second - { - // We build a new ProcessMemory instance here, but we could also - // have an existing instance passed as a constructor parameter. - _myGame = ProcessMemory.OpenProcessByName("mygame.exe"); - } - - protected override MyGameState ReadState() - { - // This method will be automatically called 30 times per second. - // It will update the LatestState property of this instance. - int hp = _myGame.ReadInt("mygame.exe+1F16C,24,8"); - string name = _myGame.ReadString("mygame.exe+1F16C,24,1A,8"); - return new MyGameState(hp, name); - } -} -``` - -And then this is how you would use the `MyGameWatcher` class: - -```csharp -var watcher = new MyGameWatcher(); - -// Set this up if you need to react each time your state is updated. -// Alternatively, you can just use watcher.LatestState whenever you need it. -watcher.StateUpdated += (_, args) => Console.WriteLine( - $"{args.State.PlayerName} has {args.State.HealthPoints} health points."); - -// Don't forget to start the automatic updates -watcher.Start(); -``` diff --git a/doc/res/CheatEngineExamplePointer.png b/doc/res/CheatEngineExamplePointer.png deleted file mode 100644 index 009f45e0790a1d82b0d9a41240062e1f47111e0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16298 zcmch;byQn#vo;*GNRbu_#i2N*PzVHfcPnni30gEb6p9yjFBaV0i#rr|DDF_)8u-%w zp65L0JL~-MuJ@03EwWfi_P+Pdo|$W|xn}NAWko403}Orb0DvVUEv^axAcVqSduWL8 zPk@A9Ucvt%IH^jB0?Nlo_u+4lEkqPV0D!6}%-=>R@b~DSq_v#@0POBRFNA*kLQ?=h zNJvIp_hhr_)S=o? zaunqG0XS1gp1t!QblF|%f9O9?=#Lp6k8k769j;G+VN7uQ-}4_ z9Fvl0a$&4%XexeQ)c&f^VTGXgQ`W4|)$(MQkaXy}@7ukA{LcZQ&1ZGl=1pbl5Emjd zfbewHkeXzdc-|y5>*@Dy)Z?P&bx%v-?_X0&Tn+8bRXc~5HS5DIf(^%k1zA2i9CAaG z^ZEPN-h(}sDnTBSx8m9Hd1CVAZZ@2h)}st^Q7`TU+ZLabZg--V+m#6{$8&y>qHXbu z#4g^E;#FFoSM)&v#J>;XH=~gkXR`IUn$Z-xC|Lse+;P>88cqxiWy0!46AbVAEFizH zos{CAe)kXSHrOQ`t5yOvsO7Mscm=hX90(-#S&EmTRm=P|JEoh*yNdgw92v=L^aq#b zcx0S5ERuRZSTZ&v+_v*!;iZnYdO&P~#%6`tfR@t2jj#<#M^76zoU(?N9 z(9^GAp?GyraZ6-F){-?*i5wErVBhh^?^-l~cB1SDcNLgd2KmZ6tn}DULUk*1Qp@fY z+iLRV1dZ5qfBaAm`x9s>#0F7$s4;GhXi$yV*5jzEpYCb*J8V$s;jV6?-S_AW_o#n4(h%(pOeMi1Rx8IpN8Fq;85!ptk0%pd3}PfIt~zzcs{#jYi<3u<&G=raXahx`I^u9*}2{Vb(Ggm%DDG= zIF822n&(!8n&IuOMs{GRFOY7N6Ck{WwrBQhAWli>YM%P^>9AwyxT}*+4m~B>s-b-j z9T4)I!`^=!10c-vvj!2MjQ=0V`~1J*pwTJUO0q(SD=Pqi|MmEa@4Y=Dpb)JV9q{c< zdo7qrMLQ)qmPFCgV(Mr0@{hApIG%Rg{(x?Gw%@sVwd3d@0T^EI*MaYb3THjGq)R{^ zC>zKC23&AmsN7U$aj}4_gATb(u_*_Z&TQ6Ip=qc78<81)x@nRku^_rMW!4B*oB`#Q)=V861Lqy%v11^6-m(2 zNK=eSP1VzhDZS83fU;xWEM4m0$eyFB_8Kr3E6TwELtbwz94(bt-9@olX4dE+9mOH? z@0C>3pnX9k9ul~HDaW=yH$JG7rA-tl2Y9o4U6j^}V$~YmGchv{z$<0uO44@o( zJib^&p4^a2^KpO5UPh(lBVVf|Uqt@>Q!_1-n@D7Ht!}*bk)4g6OIQS+2eW?uxk8R` z8UVl)8w&Ei3b$@Ev`Hc{zL2}Dng7t)kVU$ib}d^5yO9{3ecYYj8H1L2kjQD~ESw54 za=or!?dv$u-Yn%%WRh{}1OQyliWFYKq&jLf3KRwu6g*E-dhM;r1fCkpce4l~z9M^( zP=VL4zY$-n0q^|kWgMENzVpK_}y+;wYzTHvbM!uN$MlBQ+QSpK8!Fs36ehS zL%rFTYs6Y=>92t@9*%366#9vHY~Nn0nRK5Kv?FS5#u$_$=pP_YL-%ilidJFTf?`x= zZ0eGG*suYwADB_ATL_x{4@!m~je81!?2nU3CA*n66SrUc;&DDtq`he0=fqchybG=# zW5sz%C}d|8>?`tsDaVZ-|G7+2t3@GxA&d|6G?bk|!GA36@ua_{T#&ct%O>V{hrPIM z`STdT79Oq}m8+Urw+IxCH~yOlE5TEx=biaHWrqzBUmGXVsAwhI_v7pYnohc?%}x1l zMm7+RDLd+iT_l;v@R6MK-Ov+XBHQ`*J5R;24|;2tXmM;aBHgMek+12iwCVb|Lu?^| zba;}CMpbE=Vg5IILREyRubx7xIN1`3n`Di<7$b4Tv>u_ISEV!}J-G=n0x2En8=DVYk z>4T5=;#vbC4&9r#JG=7avtSWhji$$jUd6tLv5jzh{73NhHz@2gss?!hsay59*Ry`xx^!AqcYw%Rl?A{8V=g zu9f){{XgiU6W4zC*+VDk;N%u#4R!*71I@po`UgDMhy4@PI%$^)+dMqF2AMFT=8m!7 z4aflT@Y)M#(5m*?ToQFv=KV6W@<1X%+L3%S02Mu_Tnem%2zV3zlwAG?mJAJ^0RZQ6 zba&8VfwCa(l;METqIjnz^=@;qP-OLqL^j|?Amc)`ri#K+B}hz-!zbAc+w9sR-T##A zOD+-M1&tPq7n6$6B9n33i?S$jqXT)RuO#7v2@q-NuLONM{J@Jk3;nkjBbS%p51x>E zO_M~^2pB|a<4treXCCmtBKbJO9{9x)YLOkN9@%5zF(y5T2>6mEUl+={(b~$v3I&NT zz1Q7LDT7*J1l4<)In%8k%j0vpl~phP@JwtdfoA}E2+YVLe%9%kn9y$zuDS}6OlJ20vCvTkpOfRKt3;wf_AM0eq)%kxE7e|7-nd583 z5IL8YzMHf11Gnvd_I|Ck-&T9a19a>jx6Et4cYe?W2!D_%QSdm@SkUCN&hwjxxTdy? znK$sz81lzcr)->(eo`IFBR0mgsqc-%@9OUYQWM&JALCx@YcUCB)@8u};0lDbU?a#7 z%aE^rL zGr037>@*r6DcUevlZFY_h4VIuA@rpBOa`FxLiBIP~7NBnk&43ZQ6^uSCMkJQ3d%Sa0z~c)m(-jGj>^a=_pg_9Aw@()7>M7EA+Vz@lw?O@f|D86_<$Z z6P#ErDku^q^KCj;RTD0X^3J>g@$1USi-X-m4*k|dRK<)a8IND2Iq+Ye3aHTcy0VYi6Tt`NP(7#j01~h*c$+dR#FAS zd5r#;w4U`|hFSjL7*i7t94OF@;d~l?QjVMYhSB2moPo(|CI; z{z*Qii*J9Z>8wC`Dfbjj#w}VFi0wW<2m#>d%RU^ac?_PB98V{jVV>jwdIU<@4r@wU zY`mipSD#_CeVt7Tpd?|Rq9bVR4GK14pXmXIydyPV*Y#Gi5g&;me$YOVL3SAkLIU{z zlu9FhIA#T%EyTzi$H-V*YRoW}p!GNd{6+>pt@fxYND`33pG}~z?ZzqdOjLcC{Wd&| zv+{IwPyUp0v;fC`Io38BIRMT2e3c2f7Th06=7{8{_X&3IVu>e>304cb`-Adm|I=!J zO_^djHIq4-t~s`r$obOyS;1~`DGI>vJKP2(0%w7EaP7p&qVal5rGOW_bXU#?S>>t1 zRe~zg7_n$00RV(T4-4a7)>Fs2m`jF_vjj}XcD$cV&JqhOWy3eu!;-IBBUi z?W}p}H_d;y71v$|9&Rw0UG+)}9h(^pf0pVRu0v?ETM}HqEcg^g+h~FZ)ihWj?I>K~ zzRMxMs-l}^_|b&b)!R?u`#XT*_eV!ZRXZokI3{q?GU|7(N0a7v!HONf=@^H18bQm4 zv2-lFJ7e#H4FoTzap1@Mn z`Qi5s;U&^BY4jA0zqza~cALZQ82FeW^p$b*FXI8cFX*PBhPI2H50*QrmZ`|QE_se0O<0Dxa2nYE!ErQ&BR;4c;cKguncwA%(dh@Lhh z{5vT@I1K@y{bd_(erY2FqOMJSu5Gq@6Nl1#w?Bn>rb8?|2_FIm1BUI}9O ztdbfyIquQK_Tbx^yL4vaZ?OOdNM4L`!E3J7jHA+6-60X#X=udq_Q)(t3^$`sCjg&x zt1G6KM2u`l6Ex zOps+6HG0$#47W218QU%zLeeK27mrY1Z}BlyRdb2KW{Y)GO*Tb z>UWZMWd^M0M&CgjM;>36H(>>s3$cswFD+3X#zvRFeKP$t$*(-)Uf*EkHo^lrS2)Q; z0)(kB*#6-_kVjVBD@=ujVCa> zF^V=+D`U3!8DnI*C9|p*$IB^eJ)-n@w{(st4h`0UaI-tAhZdyFhNI92o?9UTz)Yv- zIuE5GPFVgeVe^Z!&y&=!wbB7q*>4K&9?EK=vZ=ab8wv8t9(faV%FpLbi=@okH>i_2 zKF8AR`YJuE4(`yU{QUm&B+4Q@-$ka$HT+S6x82r<$Ho2>#Q$W*|219ypMN;Jg#uvc zhjcvXyTO@Ld_XN@16zjkz;<#ujGP$`_47P|?Q&B#bxjQ>MZ#ByYZ1MJa#?q&- z=?N+78C)@*a3{GL2m!PTxVCSMtnBUA>TQ>}I_?y-xBc(V#=jbTZU+DmW2!(zUDI=O zH}fV@gNxIZXo@*$C&WN%gV*s@loiJHHr@w0##0^d*@Pa0W(An7(q!M8*q(qntZ&10Q}(qjjXb3`mPb3PvSi%Zv@_`pVP~GwVPbs85*R{ZM^HUjF9rcJUy$H z`Rle-wB@6$Jn`XVEDw46@jjLD0)JR@u0ozjp15eH7aAUw(>l+U_t$W3LL|FbKs@UH zg+dVj9WEVfak0tbuViUpm9$cp*xeV3@?b}RpK7NjOZw$rhXXqSourA79PsShSF_09 zBVG*6K*#l?PYS3m<}73YT-#p;+;9nO@@X`TIscFLKkyFx54w##W&Tm)k^Y+0J!-!l z&a($!4j`@FjX_M&{m^0r*W%4GAM%q7-q?D;uI9Eu1B~$eOl>>#xF($lP)0Retc7cU zO)*q+6^6l}?N?%H)xWu7NdOE;j_d!X@BivH#DONNB5SNa4myzll#yli)1*TbdkN-V z&ovjj_pUbiXs{V1ackuqaMV>*JLcX=&DdN3FQ+&dd~}EKyrv9_6lldq@r*}9wma!B zL5bz!HR4UnALl(beTRjtzEU{dj6BOuRu#$Twc-vpwxSXe=I`Ic>VZAFYH^u@aS9Mn8 z*JG+!4=G1_`L@JB?>dUJ)Tls-UQf)!P3oLc0jKTI?XdQ{gj=sb?@^K(F7FtOHuLL8 zuQWlMJy|BMch19!kZ~)`Eo{i<`Iml9&&_5>-ukbQ z$u@QSR^xAX?S~z2lN-It?b^qQyVM(w=2#-CULISo&5fi5MlJ0c?l&Qd?4rHWA>+ns)0b=4|?+8VhwIz-C^I z)772bB*xTzzjF0)ct=%+*@%@AaQZbT^=rPnY!91VV7>KHnMp{z^_U|rD^j`M*Vf+8 zYFm4+H^w>a-np>i6L|)S%hWqwXm!6ZogxXY9|%)+v{U@z?12U^0*s#Wi6@KO7H^fc z@@1AB_adM&_q*tH$|<5cxVpZQU!Jp1GwG%puBn}(VGWgkwJP(}D7h}R%-wDmj7=Hh z|ELzaeLj(rz)|w@AR;Mgh}eoEthxALCMEDHX9T%?kl>BGp8Je!iN+Z0^H6`#R0bP8 z{?C1$Z=N;`Ny7rX48M=*>7zjt1?)-re2|mNlSCJzd{`sezA0J*N9Ex+*UzCdi|xaQ zymW49%GE7NkM7k344`^X&@V$1Vv;_}BiZnn+vBRw zRqVSl#^vb~UWFsS&wou`zDf@!~>ZSV-4x+z%3SLeV_<$^6qo ziKD;6Gw)}t4~BIuw&xD;Db84X&4!1%7s+{A^&-HD966!VvYQrj+Q;e6UuEVJ)bM9@ z491lvpeuN5XL5)=&TzBUXiFU1oWE6actCk}i2LPrsTd#!V^$iSi)W7RzBZl~z|A2L zpWlUOi@}x5^|{dh3ZB9L(ah0iF>%?BP7^Q%fwNB;V-Z}YSM(zLHdGF^8mYqy2 zqT1Ic$|b3lP}h#@km~;sv?R|42{?KujVVrP8x{+JQt~FIpf;V2)vB7(O9OE>>^8r5 zAeeqE8A$MH&P{Y2R?p8Fh>8>in!HS+$DHKH6;I3A%^>0&mJc{;1D4^>p-)mHC5S65 zqef4*4{m4d+wF()r>``Y$!x+_QElg?xAQ283GxDILqHP!d}{Qf@T>7$z^^7xFTJKE zSz!lA>EWBo&8LSx#IYT@*Qc55T9hReh4`a@jo=;mbog&O5bt51qQqHE(Fof>-Y0oN zjegW!G;)7iE*tcaX8#5(l1hPS9Y^_lHH9~(a)e_b8v=a=dzV}<$}7?=Ge=j1uvI+8 z2mGc^a}`&_^lE!zpOdGa@*S>LFlEn{5g8)!^X+$!{_%VM!OC8hlCq6(@;}iJWk&ev zpChe3B=iINDc~wi_(SJCoErWk#UURC2EQ(a`-bejm)dnS=nq-QQ>aE3{=)DD-cb{V z!ufV?I;qO#-d2$eDGvBvBSq{tuaj(ad&og`Ed9 zEPrc>sB9zawOSkZVD@s;C6suj`(utt1&HOX+^Ccqv$?r83oj08bfQA;D5M8O#zQRWPfT^1yfA>Vd@?{l^o6h+!ek^v*=`9$&doj4Y1{eQC-fD`!cD_T+ z?R7G?%jgPhvaURSpalYLcLY789DC1GMl6wHK2%f?JVt7)NjbH7Thn%GOe9JW+mI8c zJm1Wlc8e3N)E5w+R0gsdLeZfDw_C-;jmDENr|%owCn&_2F4 zK!@bR3p4ZWy)KRbQ)V2EAAy`9SmqT3-_IQKeXrZ~$oW3v*t~EE*J~a;z1V7h-h2IK zlu>(GGOra#9dy5%l3sM~&chvN!W4+2!FU{N+5X9GUF3<}09uw88MR+4c0-ANKqMo^ z`ynXxkuBmEyW{K8MBznzfm3-InvpCTD^CmdA=HN<<`XhWAypAm4KthGbE5KwY-MSU zL@aU^djaFSXjZyx_Gu2*$(25lkHHhAXgLZK7h|T(5&Eg+p6!bS{o0!?$~nalx_}@x z`K*WcUwyXhJLKi6vhB8Zu0YKlo993KCT2Yo6;!%xM!LR3%=xe(xTSP)yJk)y=<6MBFbIk@oe0V)1^aDK0XI~7RM<4hTQ>(L>Ti1=C0%>WMW z;;#%Bzd%?MP)-f}3;n&|BR?vCM}Gcl<9R)GgXUR`$NOk*7Vw~ZZ$EhqBz)Mt#&Aj9 zo9F9)yPh$aXj&@2on2VrWM1+-9|B#--u%?GZvs256~8mu$rQ~}(l5@cnA)+6eaVJ| zR3+(1Be0R~#jAU}-gPf2Fba&2#`F%p*~lnbB08G+GKv8NHda2pEa52Wf*(%gx;q?y zZsh#>2YqZ_K}cUpy1(FvD6Vwco9ztnR+@2v_=zX@KseifhY$2WHVmGgFGxGKQ_Dbj>BD+_D=Q#@KD37yP)t@8SdEoysp{+h<7!egzT*dM2aL zgn%4!)-z=G?x3l14}?u}`WHWPq`3m;`SDg$kg-5(V@cJ2?uI!7#*&9rO$uayVGNx3iU8BpfDC*(h2;5>~jD( zju`=$3L_hi?$L|`fk<$7W7BUGk$?6kwBjbslo59zaQD?#m&k{+d{9b z|8dK|(-gAGX*ND71du6$f!mgo{)H9Z}k}J$? zJ?xuT76N&C&Z;EU?=G<97IVa0@8NX(Nvgqds-JirY^49{C%ENw*z5e>maYkceF%Ko zN#!|VvEjgZcYW%-^eVnK$~>kOn8NoRiehvoEo4#m){#GX_`Kf(v`;5To*7wwfyzEn zn-Tb);CFBn6hT*8j{xX7kP=e{3S8xjTPSJlVZ0Pf&Au38CgE?T0vS6H6?*&t$J4%r z)!b~K&p^jh9carhJZjGZwwX0PmKPl=c8+k??7e*hPs49EbBREw^7ZNMIJvSrXsUvs z?d|31`Y5znYVTPV$l8`yV1%!`D=1YeCo3U z0rf7Tlfq<$f2uX=1QgBPLeY7e=3u!~2j7cB49-4?B%6$~DS&*{K{$i_WCDw~Y!fu_ zIfiJ|xc76+=}qRiwi{m-`P9bWmhrvPt@ZdJ(I*9TUQH>}o3ir`K0&Z4RdIb5GVjz8I&phph~chs)EjQ!)Jlv2TT!BnctF`L%@j*^{s z`@5C(QjwC>KvfmKl5uB#elq=CEb|<&0H#<4+z4b{5@{jyLfh47C$Z&cKZ|MHh1%9K&%HQ*`?Gvv0uC^ho zpuigm|C;ysXP`vJdTR1J%gW2CVQ#$g6aapd>MS(0y{DF${kfPzRoJq4zkzMKZ9<4O zRJc^$y!7=bfqv=8;;70;HoaQ1{0f)-nS_q1iHfRW=P7R+;z_;CTrM~`E}Yg@mREas z5d_H3CYVi?ZZ%STsFpfte;IUPI9sAnKHwc?WCZ>hKfBhUX4mu{wwK!SkT0bVj`lp2 z$JVvgAt37Gy)!V6sD7(tD7m}eXzjP;3{+FOLndoA)NNO*Q~CDkk+fPt8@) z$*?L#8`<;PM10bB6&PH{bA5OIj%rkWG-H}o+nLb#R9+P-_s~Li=u3k7EW0}ULXz=R z{yNl_Q?Itur=x6DoUYxAXo3BK95rz;>S{6^KdJxBg_b<}l3_C`WlMx-0?VG?Zoy^s zTU0OW=UT?C)fBxHmPUTHuU=}&@|dx?kq~IXdX5i13W{A#--j`Wa?nDm9kjL81}MvP zXU24(m>N$>kJbdCllwf(ql(4q?u}y+u~~YiKcM`b4;TtRHGW+NTqeIm+W)EU4BX5( z%AC=c0%l>Q_C$2Yxc0IU%?mL!M_86|lEt;fXo5g{55ZNw^rMOVw!2oweEAWq;1r`1 zk~jEgW2QXj!RtA9O^SP&@ph#~{+wQwG4N-!sb#-8;{5k#eHrpeks*)ABe*wPS4>qy zR2qSU^?qoK{RS(CIz*pj?z*~6ImKLd`fRLq*Qk*}2P2O)ce#3fIEgKEA8M#1#VRD- zG%P9{ESeSr#r7yVDUK1Ef>= zVbM9p46?G}D`1>Na7W0J1zYGvPH*3&6%r#$x2q)Yk0p9`Y9*;90gsiKzo7)@8S{$A z?KZok=dwwkY5UKA@IR@vHlIUyRYQ-;WRRjOd}g5V`ke8HVAD1_@I7wH7p_xloHPM_ z4dk0K4$UUL^1U>}(D86uJrujFUemGgZ4DB+;I}mW{F|n`#YswezDiPa0_>y^@wS9B z;x?|{uRxyqU~}^PFitRT^$fJq!vdQBUvx$6MH`T))a(#OU6u&4%aVX#^_!WbEMIq{ zM<|`(rQ}(#rr+#!Ixcv150SSs*vc(0xprdKYq<@Ds#o@t$XZW+G@Ss4v;hSi-2PN1)CKpQTNoSop5AhDHSB`%IendmQ^Gf-YSo_bIy`J%_IU} zb%hrvXq5fNXo)zlGNQPSdl{SA_1dDDA{B-1O<*nEDD6M-CR6EY!Gd+fTd4nY>47D9 z;-KNzYRY|OfuUuD632+jF^8Dg*V*FsMV5L{AnR$~|ihVN$Zs0%KC z^3{k`y{F>&*|7|_pv7pw2T&qNQ<}>)Yarc&x7wl5ViejpSe|l8U2CR&J@{se>u^8Y$*fp5GivTLeCa0Sf9J$*B%f%X?*T)sTGX`5TY71G z0V?oGCu9sJGe)WZrzOFcIz~8Natwcg&;fdLIqDcAAasVhc7H1HvHG7GVC_TR{G}Y1 zcgKFKDU1(E3FWzZkEsR%1%8~MlZk$!ZDW-EJsp-Td5gwtiq#b8 zTYV=;hjZ~;tS~r-=pT()oX=nu_{|C9<7+;NSr^Q0Yk1>sVGeUc*+zjoau~d1D2A5| zvzj-oAKOu>fY(?B5NJzZL_=J@w~UPtOd1|+BB*WJMu&j~=~}OTpE+W{?fy#pl0^G- zr6Ng>lB+#A&ed|8h}>r>U(@zPRjCPH-f8p2CZpvFF*4&(Dl5 z$1Z!E34WpzHpJ0p`E9ZHd`f3~-*h!L@_5E%tZ)m$xg#5*AT!0&yELMWcx_3chO87aq`qq&@5A9yjH|1+a#rpHwlxE9*}#psOW zjp%O1z6$k_1~c{4(qL@)P;OT7&(##4@-QkT5g|#{6pI$HPmH>)X~6cstMd5mbK z3jPf47R|`gH&f4z?ithJS0E)4NN2t(VgJn;cU^NZ&NmgARdVh(w%t)hIUY5KG?#nP zR{(!1oxI3YbU25&KUm)~Q^EY6B^ds@P?ai#t3xDVcxH@{|9h`g9ZkcHOh7|>{@E%o zbU(U}1w~vGKF9S9HDv$4bj$xV8E>bdq1fh>O&pbS%ojovM!}^WhV)e z5Ag!KfLz}S#D@xp;lT;kW!LOdRq}z+U!||fbgl;iuG+clvD?PrTzWiLNdcMQS!k0_8B(knJMTq5aj>XbMwd%PX z%0Xq{4>k|i0%PM~J*07UF-`XVsCV%TyYRur$?6l(k}1a^=h>BQq>Hy*zr*w0DjzEw z*s$!J%5Z=}P9J}Mi^Vq@ratEx{r;r_u?>xVqpFmyWTzZf6-YYz;N;lZGcVl14ovj$>YTi=eKBz4$8ZRXIshmhq&>xK2I!dV7>dT2a+HH&-e!@=9&f z+#TGWov>sDs_(II)*VG{DT7A&_z`b;t^V0c(!cTb?0R23%j`85j!FWWR#3C0F%3Pk zG0j=im(I!WMNWT(`oWZ-YaaXH5BLBB<9@UE7&zAJd*QK1A;85 zSS*vq28R%Z8#pai)uH98yF20l3sjr2)Pr)&2UfNRB@A|_x*i^|@^CUO`4BOSf@g8@ zb@E0Jjl^73@xklRuqzR?$wjvHSQ0T9!^+fr5?MZHbl= zq(yARQd zFn6+5Q%iYcu_T{1kkvCRbfvVh0&U@g%olv(pVn!TL2wirdKnNZx^kiTPDa5A@(xWDtR zoSywO0d(P08=0=s4`Bcpqec(T6u~EEA?*=lZf;cOjmIQ+lqq)Xc`_k!A8LNiUuZj@ zjcCM)=o~a6gq`Dd28MrJ>qoFWxQOo&Qz=?@IwO3 zyt>J^vM0mnbNpt}w3B*`khFs!O`eyztJh?*EpcIV^1Gj!y!g=|XLM*pBhYp5$f(6? z-gbYDLOD7OJix%VLu7ugGm!B5QZoP9eRkZtMfnkq=hf^kP>a6ggXxpVULcOHLW1&S&F+7b?=-H7CnF*2!^* z*R-#NRyyx^O@?-kIz3bv{mcLCo5Ac6vF%t(R@}G;`7wEE+r&05S_qGdaiFK+ol!X` zTgSi-j2L~zgoTJ!QU_>L9Jfo^m?rgQ>0Yb7coaycxP_R>cv18RjfP^g!KZCeOm)j{ zSqWKLSbePY-mgp)NFIIASkWdw!h0onj}WFj;(}&L=j3jvQKfk;;Kw{ml`Q)qvhMZT zsAfG@ji^_Tb~mNUfkEMENk&~5DWf4wl*_ujffM^$MQkbZ(Jh#RjM|j?f>`zH`E+if zbzaD_Ehy`B;RaACiJ=^8To%*Saar6{yY>h&qBC7X0tkb{auhb?Ds1TZ;RgYfQ*+<~ z5FiiXuSEG*3xaR5f&ax*kMhA)NoD;1x#vbE&fep&ticrhb`w5P5KGJYiyugPb%Crx z>&=qhHyKIBw}mR*e|0!FWrden;5#X(gsTqk8~j@a`0vf9KifgpYvC>PolybJ?I66x zyP>-!N3s4IRcD&2V}h%kAsuXrL_Ybm5e&&o3U1{XQSj99#hM}ar;qCo3yt5IlQfh6YUpz_pk3XwP=JXavMys*0mYG+1Zg=F1~AcJ-z8l+V@3U{7% zmrGI1hgcv>)ni-;qZre(mMa=8D=I}MWi)irQ)-&gsuMkofO7DJK5t~qu|iIc(F04gMkcp*RnQum1%cPonN8XV8N2(Rw6is}tDv76SU2kX_plYZ3)K?j0c)2J4>UPf?J{Tw_-CE)G5IUvT zq_tNtUh(yPG+EBL$NbdRvk8NR-I+m@$3iusI92zb!aNw?6fR{v#GbB}e{T$uq_VBD z2cqC#2WbCLX-UA5v9Blf2vF#ZoGiL}?N4h<8|Ld=a~XMc7?7@acY?i2|KKQ!LlAeZ zXN5yrDE?R%Y9>EVBP^Bm+-5XnItR%>@4Jl=}XXjCf6{r_M-#B<^d;`ARyWQdo9l%S{Ges$l45))D^JFYOFA zz>GhXhgg&TCZQmZteTRXK<<7oj(|>)kad=r=OkVrX&LERu^*?ZBl@iL;5#kqd{ell z_Fv7q|3v0Ln*X?Lq|Adr1~Y>1PtWE_FQ0zUAub~0Bb?&? znWX;DYJab;BAj;l3W`3(7cxVvyX?(|F)Wt`ik;OReKAXqD!j*751Q~huV3PdISDsv zz%g#V+}J4DP)t#nT+$k*@Y}-IOjBJt23m^ubJj>$HhDiXT(oeaxDIEFQ4#6_&Sh2V z7>icgmnTb5XYs+0t)4wlD>RJKJVD5F)W?LR@#~VQasAoT8*g%c1$0n#H`hN z)g95F;kebqXvfC3%2MV8;UfES%N(oJV#DP9FpDUYT$%UPP=1yRu%2JI7(Z+0dzj4? zWUbWx`Z2lJy8tmp(IhSlqG*%k($4c8XG@&&2<5w?!>)qh(RHA`>mI$*hT^-F?4{jt zwY%93EH(wx9A;Oy&iqk=Mq9XBE?5IX2zn#;z2=YYU&Iwu4&$vbs%g8Iq(xb+y}?>t zaS+{%S(xZu{BS{c_q-=t?`N`kxL5=eAP0Gt3O)x As an aside, if you happen to enjoy the demo, consider buying the full game to have some fun and support the team behind it. Also check out the sequel, [Slime Rancher 2](https://store.steampowered.com/app/1657630/Slime_Rancher_2/). + +## Install Cheat Engine + +Cheat Engine is a popular memory hacking tool that allows you to inspect and manipulate the memory of a running process. It's a great tool to learn about memory hacking, and it's also useful to complement MindControl, as it provides a set of tools to inspect memory, and lots more. + +You can download the latest Cheat Engine installer from its [official website](https://www.cheatengine.org/). The installer is safe to use, but has bundled software offers during the installation process. Make sure to decline these if you don't want them. + +## Setting up the game + +Before continuing further in the guide, make sure you boot up the game, start a new game, and clear the tutorial to understand how the game works. It should only take a couple of minutes, at most. + +When you're done, in the game settings, you can turn off tutorials for the next times you'll boot up the game. It's also recommended to set the game in windowed mode, as we will be switching between the game and Cheat Engine a lot. + +## Next step + +The next section will be about setting up Cheat Engine and exploring basic memory concepts. diff --git a/docs/guide/hacking-basics/basic-memory-concepts.md b/docs/guide/hacking-basics/basic-memory-concepts.md new file mode 100644 index 0000000..a8791ac --- /dev/null +++ b/docs/guide/hacking-basics/basic-memory-concepts.md @@ -0,0 +1,61 @@ +# Basic memory concepts + +This section is intended to cover some fundamental concepts about memory hacking. If you are already familiar with memory hacking, you can skip ahead to the [Project setup](#todo) section. + +## Memory and structures + +As previously stated, a program is loaded into memory when it runs. This means that all the data it needs to operate is stored in memory, as a series of numeric values. These numeric values can be just numbers, like a character's health points value, but when grouped together, they can be text, pictures, audio, code instructions, or anything else. The way the data is organized is defined by how the program is written and compiled. + +![A memory viewer example](../../images/ce-memoryviewer-small.png)
+*An example of a small portion of memory shown in a memory viewer.* + +A structure is a set of related variables that are grouped together in a set. Each variable in the structure is called a member. Structures can be nested, meaning that a member of a structure can be another structure. If you think about it in terms of object-oriented programming, a structure is like a class, and a member is like a field. + +To give a few examples, in a game, you could have a structure that represents the player's character. This structure could have members for the player's health, position, speed, and so on. You could imagine other structures that represent the game world, weapons, enemies, animations, etc. + +![An example of structures](../../images/il-structures.png)
+*An extremely simplified example of structures in a game. This is not an accurate representation of any game, just an example.* + +All the structures that are relevant to the state of the game are stored in memory, and the game itself uses that memory to operate. If we manage to identify these values, we can observe and manipulate them. For example, if you were to change the player's health value, you would see the player's health bar change instantly (in most cases). Unless it specifically has cheat protection, the game would have no clue that you changed the value, as it would just read the health value from memory like it does at every frame to update the health bar accordingly. + +The trouble is that **we don't know** where in memory these structures are stored, how they are arranged, and most often, what structures are defined. This is where tools like Cheat Engine come in handy. + +## Attaching to the process with Cheat Engine + +If we start the Slime Rancher demo, and then open Cheat Engine, we can attach Cheat Engine to the game process. This will allow us to inspect the memory of the game. + +![The attach window in Cheat Engine](../../images/ce-attach.png)
+*Attaching Cheat Engine to the game process: click the "Select a process to open" button in the top-left action icon row, and then pick the process running the game.* + +> [!NOTE] +> This is not the case for Slime Rancher, but sometimes, your target can have multiple processes. The correct one to pick is usually the one with the highest memory usage (check in the Task Manager). You can also use the "Windows" tab in the Cheat Engine process list to find the process handling the main window, which is the correct one in most cases. + +## Memory viewer and addresses + +Clicking the "Memory View" button above the bottom panel will open the memory viewer window. This window allows you to visualize both the memory and the decoded instructions of the game code. + +![The memory viewer window in Cheat Engine](../../images/ce-memoryviewer.png)
+*The Memory viewer window: The top-most panel, highlighted in green, shows the disassembled (decoded) instructions of the game code. The bottom panel, highlighted in orange and blue, shows the memory contents.* + +Let's focus on the left part of the memory viewer panel for now (the part highlighted in orange in the screenshot shown above). It shows the memory as a series of hexadecimal numbers that range from 00 to FF (255). And as you can see, there are lots and lots of these numbers. You can keep scrolling pretty much forever. This is because each of these numbers is a byte, and this game, like most 3D games, has well over a gigabyte of memory, which means over a billion bytes. + +On the leftmost part of the panel, you will notice hexadecimal numbers ranging from 00000000 to 7FFFFFFF. These are the **memory addresses**. Each byte in memory has an address. If we think of the process memory as an immense locker room, the memory addresses are the locker numbers, and the bytes are what we store in the lockers. The address on the left of the panel is the address of the leftmost byte in the matching row. If you select a byte by clicking on it, you will see the address of that byte in the bottom-left corner of the window. + +Memory addresses are a key concept in memory hacking. Whenever we want to read or write to a specific memory location, we need to know its address. This is why memory hacking tools like Cheat Engine will always show the addresses of everything you inspect. + +## Values and types + +So, each byte that you can see in the memory viewer represents a value between 0 and 255. But what if we want to store a bigger number, like 123456? We can't do that with a single byte. + +Well, if you group multiple adjacent bytes together, you can store bigger numbers. For example, if you group four bytes together, you can store a number between 0 and 4294967295. + +This notion of grouping bytes together to store more than just a small value is called a **data type**. A data type is a way to define how many bytes are used to store a value, and how that value is interpreted. For example, a short integer is a data type that uses two bytes to store a whole number. A floating-point number (or "float") is a data type that uses four bytes to store a decimal number. A string is a data type that uses multiple bytes to store a sequence of characters that form a text. + +![An illustration showing the same bytes interpreted into different types](../../images/il-datatypes.gif)
+*An illustration showing the same bytes interpreted into different types.* + +If you select a byte in the memory viewer, below the memory panel, Cheat Engine will display the base-10 value of the byte, but also its value when grouped together with the following bytes, interpreted as some of the most used data types. This is useful when trying to understand if the byte is the start of a field with a bigger type. + +## Next step + +Now that we have covered some basic memory concepts, the next section will be about searching for specific values in memory using Cheat Engine. diff --git a/docs/guide/hacking-basics/code-manipulation.md b/docs/guide/hacking-basics/code-manipulation.md new file mode 100644 index 0000000..561e188 --- /dev/null +++ b/docs/guide/hacking-basics/code-manipulation.md @@ -0,0 +1,135 @@ +# Code manipulation + +In this section, we are going to manipulate the code instructions of our target program. This is a slightly more advanced topic, but we will keep things simple. + +## Code instructions in memory + +As stated in the previous section, a program is loaded into memory when it runs. This means that the code that the program executes is stored in memory, as a series of instructions. + +We have seen before that these Assembly instructions are represented with a short mnemonic, followed by a list of operands. For example, the instruction `MOV EAX, 0x12345678` moves the value `0x12345678` into the `EAX` register. + +But these instructions are not stored as text in memory. They are stored as binary values, where each value represents a different instruction. The CPU reads these values and executes the corresponding instruction. These numeric values are called **opcodes**. + +For example, the `ADD EAX, 0x11BB22CC` instruction, which means "add the value `0x11BB22CC` to the `EAX` register", is written as `05 CC 22 BB 11` in memory. The `05` opcode tells the CPU that this is an `ADD` instruction with `EAX` as the first operand, and the following bytes are the value, in little-endian (in reverse). + +This means that, if we rewrite instructions in the memory of a running process, we can change the behavior of the program. + +## Locating code instructions + +Even when you know assembly, reading and understanding the whole code of a program would take ages, because there are lots of instructions, which are all very basic, and not very informative by themselves. So, if we want to modify parts of the code, we need to know where to look. + +Let's take a quick example. In the Slime Rancher demo, running uses up your stamina, and so you cannot run forever. The first thing that we are going to do is to modify the behaviour of the game so that running does not consume stamina anymore. To do that, we need to find the code that decreases the stamina value when running. But we don't know what this code looks like (what registers it uses, what values it compares, etc.). + +To find this code, we first need to find out the address of the stamina value in memory. We've already covered how to do so in the "[Finding values with Cheat Engine](./finding-values.md)" section of this guide. Here are some more hints: +- Like most values that are displayed in gauges or bars in games, the stamina value is a float +- You can sprint and then pause the game to freeze the value while you are scanning for it +- The number you see in-game is rounded, so it's better to use the `Value between...` scan type instead of exact values. For example, if you see 72, search for values between 71 and 73. + +> **Remark**: Once you have found the stamina value, notice how it relates to the coin count. They are very close together. It's likely that they are part of the same structure. You can even duplicate the coin count pointer path and replace the `+80` offset with a `+74` offset to get a stable pointer path to the stamina value. + +> [!TIP] +> If you cannot find the stamina value and still want to go further, just subtract 0xC from the coin count address. + +Once we have the address of the stamina value, we are going to use a tool that we've already covered before: the "Find out what writes to this address" feature of Cheat Engine. This feature will show us the code that writes to the stamina value in memory. This is likely to lead to the code that decreases the stamina value when running. + +![Using the "Find out what writes to this address" menu in Cheat Engine](../../images/ce-stamina-find-writes.png)
+ +This opens a window you should already be familiar with, showing the instructions that write to the stamina address. If you go back to the game, without even sprinting, you should see an instruction being called over and over again (probably each frame). This should be `movss [rax+74],xmm5`. However, we will disregard this instruction, because, when we start sprinting, we see another entry appear in the list. Because that second one only appears when we sprint, we can suppose that it's the instruction that decreases the stamina value when you are sprinting, and so that's the one we are looking for. + +So that second instruction should be `movss [rsi+74],xmm5`. Select this one and click the "Show disassembler" button. This will open the memory view window and show you the disassembled code around this instruction. + +![The disassembled code around the instruction that writes to the stamina value](../../images/ce-memoryviewer-stamina-instructions.png)
+*The first line highlighted in blue is the instruction that writes to the stamina address. The next lines are the instructions that are executed afterward.* + +## Removing code instructions + +Now that we have located the instruction, let's try modifying it. Our goal is to prevent stamina from decreasing. In theory, this can be done by just removing the instruction that writes to the stamina value. If nothing writes there, stamina won't go down. + +In practice, the convenient way to do that is to replace the target instruction with `NOP` instructions. `NOP` is an assembly instruction that does nothing. It's just a placeholder that takes up space in the code. Its bytecode is `0x90`, so it only takes up one byte. To fully replace our instruction, we need to replace the 5 bytes taken by the original `movss` instruction with 5 `NOP` instructions. + +This procedure is actually pretty common, and thus Cheat Engine has a tool for it. Just right-click the instruction you want to replace, and select "Replace with code that does nothing". + +![Replacing an instruction with NOPs in Cheat Engine](../../images/ce-stamina-instruction-nop.png)
+*Once you click "Replace with code that does nothing", you should see five NOP instructions instead of the original `movss` instruction.* + +Now, go back to the game and start sprinting. You should see that your stamina does not decrease anymore, and you can sprint forever. Congratulations, you've successfully manipulated the code of the game! + +## Injecting code instructions + +Deactivating instructions was simple enough. But let's say that instead of disabling a behaviour, we want to modify something. For example, let's say we want to multiply the coins we gain by selling plorts by 10. This is a more complex scenario, because we need to add instructions instead of just removing them. + +The problem with adding new instructions is that we cannot insert them in the middle of the code. The code is already written in memory, and we cannot just shift it around. If we were to overwrite the existing opcodes, we would write over the next instructions, and the program would probably crash. + +The best way to insert code is through a **trampoline hook**, which, as dumb as it sounds, actually makes sense when you know how it works. The idea is to: +- Write the new instructions in an unused memory location +- Replace the target instruction with a `JMP` instruction that jumps (redirects) to the new instructions +- At the end of the new instructions, add a `JMP` instruction that jumps back to the original code + +This normally involves a lot of work (finding or allocating a memory location, writing the new instructions, writing the trampoline with instructions that depend on the distance to the new code, etc.), but Cheat Engine has a tool that simplifies this process. + +For now, let's find the instruction that adds the coins when selling plorts, using the same technique as before: use "Find out what writes to this address" on the coin count address (because we now have a pointer path, Cheat Engine will ask if we want to use the pointer or the address. Pick the "Find what writes the address pointed at by this pointer" option), and look for the instruction that writes to the coin count when you sell plorts. + +The instruction should be `mov [rax+80],ecx`. It writes the value of the `ecx` register to the coin count address. This means that `ecx` holds the new coin count value. If we use the "Show disassembler" button, we should see the instructions around this one. The previous instruction in particular is `add ecx,edi`, which adds the value of the `edi` register to the `ecx` register. This is the instruction that adds the coins when selling plorts. + +When reaching the `add` instruction, the `edi` register should hold the value of the plort price. This is the value that we want to multiply by 10. So, we need to insert an instruction that multiplies the `edi` register by 10 before the `add` instruction. + +To perform the code injection in Cheat Engine, select the `add` instruction, and then in the Tools menu, pick "Auto Assemble". + +![Opening the Auto Assemble tool in Cheat Engine](../../images/ce-auto-assemble.png)
+ +This will open a window where you can write the new instructions. In that new window, in the "Template" menu, pick "Code injection". This will fill the window with the necessary code to perform the injection. You should see something like this: + +``` +alloc(newmem,2048,1E59C2074A5) +label(returnhere) +label(originalcode) +label(exit) + +newmem: //this is allocated memory, you have read,write,execute access +//place your code here + +originalcode: +add ecx,edi +mov [rax+00000080],ecx + +exit: +jmp returnhere + +1E59C2074A5: +jmp newmem +nop 3 +returnhere: +``` + +> [!NOTE] +> The addresses may be different in your case. + +This code does exactly what we explained before. It allocates a new memory location, writes the new instructions there, and then replaces the target instruction with a `JMP` instruction that jumps to the new instructions. At the end of the new instructions, there is a `JMP` instruction that jumps back to the original code. + +Now, we need to modify the `newmem` block to multiply the `edi` register by 10. The instruction to do that is `imul edi,edi,A` (meaning: multiply `edi` by A (or 10 in base-10) and place the result in `edi`). So, replace the `//place your code here` comment with this instruction, and click on Execute. + +![Injecting code to multiply the plort price by 10 in Cheat Engine](../../images/ce-code-injection.png)
+ +Once you've done that, Cheat Engine will ask you if you want to see the new code in the memory viewer. Click "Yes", and you should see the new instructions in memory. + +You should have 4 instructions: +```assembly +imul edi,edi,0A +add ecx,edi +mov [rax+80],ecx +jmp 1E59C2074AD +``` + +> The jmp instruction at the end is the one that jumps back to the original code. The exact address may be different in your case. + +You should also see a bunch of `add [rax],al` after the `jmp` instruction, but these are just there because the new memory location is filled with 0 bytes by default, and `00 00` can be interpreted as an `add` instruction. They are never executed, because the `jmp` instruction jumps back to the original code. + +If you highlight the `jmp` instruction and press Spacebar, the memory viewer will "follow" the jump and go to the original code. Scroll a little bit up, and you will see that the original `add ecx,edi` and the following `mov [rax+80],ecx` instructions have been replaced with a `jmp` instruction that jumps to the new code, and a `nop` instruction that does nothing and is only there for padding, to make sure the next instructions are aligned correctly, just like they were before. + +Note that the `add` and `mov` instructions that were replaced by the new `jmp` are now in the new memory location after our `imul` instruction. They have been automatically moved there by Cheat Engine to make room for the `jmp` instruction in the original code location. + +Now, go back to the game and try to sell a plort. It should give you 10 times more coins than displayed on the market panel. Congratulations, you've successfully injected code into the game! + +## Next part + +This is the end of the "hacking basics" tutorial part of this guide. The next chapter will now cover how to make programs that use the MindControl library to interact with the memory of a running process. This will allow you to build your own memory hacking tools, and automate some of the processes we've just done manually. diff --git a/docs/guide/hacking-basics/finding-values.md b/docs/guide/hacking-basics/finding-values.md new file mode 100644 index 0000000..a4cc153 --- /dev/null +++ b/docs/guide/hacking-basics/finding-values.md @@ -0,0 +1,104 @@ +# Finding values with Cheat Engine + +When hacking a game, the first step is to find the values you want to manipulate. This can be anything from the player's health, to the number of coins, to the position of an enemy. In this section, we will cover how to find these values using Cheat Engine. + +> [!TIP] +> Make sure the Slime Rancher demo is started, and Cheat Engine is running and attached to the process before proceeding. Start a new game to make sure you have the same initial values as we do. + +## Memory scanning + +### Coin count + +For our first example, we will attempt to modify the number of coins we currently have. The game has a counter shown in the bottom-left corner of the screen. Let's find the memory address that stores this value. + +![The coin count counter](../../images/sr-coincount.png)
+*This screenshot of the Slime Rancher demo shows the coin counter above the health bar. The value we want to get is going to be 250 (the initial money we get when starting a new game).* + +Now that we have a value to search for, the first step is to identify the type of value we are looking for. In this case, 250 is a small number that would fit into a single byte, but it's likely that the game uses a bigger data type to store this value, because it's a counter that can go up to thousands and more. + +Determining the type of the value we are looking for can be a little bit tricky, so here are some general guidelines: +- If the value can be either "on" or "off", like a switch, it's likely a boolean. Booleans are usually stored as a single byte, that can be either 0 (*off*, known as "false") or 1 (*on*, known as "true"). +- If the value can only be a round number, like 3 or -1407, it's likely an integer. The most commonly used type to store integers is a 4-byte integer. If it can be extremely large (in the scale of billions or more), it will probably be an 8-byte integer. You may also find 2-byte integers or just single bytes to store this kind of values, but this is less common, even when the value is always small. +- If the value can be a decimal number, like 3.14 or -100.5, it's likely a float. You may also find 8-byte floats, more commonly called "doubles", used when the values may be particularly big or precise (meaning they have many decimal places). Floats are also frequently used for percentages, or values that can be computed with fractions like damage values or health values. If your value is displayed in a gauge or a bar, it's likely a float, even if the numbers are shown as rounded numbers in the UI. +- If the value is a text, like "Hello", it's a string. Strings are sequences of characters. They are a bit more complex, and out of scope for this basics guide. + +In our case, the coin count is an integer for sure, because we cannot have a fraction of a coin, so it wouldn't make sense to use float or double. Looking for a **4-byte integer** is a good guess, because it's the most commonly used type for integers. + +Next, we are going to perform a **first scan**. This is the most basic type of scan, and it's the one you will use most often. It will search the memory for all the values that match the type and value you specify. + +In our case, we are looking for a 4-byte integer, with the value of 250. Here's how to do it: +- In the main window of Cheat Engine, locate the scan panel on the right +- In the "Value" field, type `250` +- In the "Scan type" dropdown, select `Exact value` +- In the "Value type" dropdown, select `4 Bytes` +- Click the "First scan" button + +![Screenshot of the scan panel](../../images/ce-coinscan.png)
+ +What this does is search the whole memory of the game for any groups of 4 bytes that have the value 250 when read as an integer. + +After the scan is complete, the left panel will fill up with a list of memory addresses that match the value you searched for. For each result, Cheat Engine will show the address, the current value at that address, and the previous/first value at that address (more on that later). + +Now, because there are so many bytes in memory (remember, there's more than a billion), the value `250` that we are searching for is inevitably going to show up thousands of times, and not only for our coin counter. Each row in the results panel is a different memory address of a group of 4-bytes that represents an integer value of `250`. + +![Screenshot of the results panel showing multiple thousands of hits](../../images/ce-coinscan-firstresults.png)
+*We have more than 7000 results for the value 250. This is expected.* + +These thousands of results are too many to look through manually. We need to narrow down the search. To do this, we need to make the game change the value, and then filter out the memory addresses that did not change to the expected new value. + +If you went through the game's tutorial (as you should have!), you know how to gain some money. Just collect a plort, shove it into the plort market, and you should obtain some more coins. In my case, I got 9 coins, so I have 259 coins. + +Now we can go back to Cheat Engine, enter the value `259` (or however many coins you have now), and hit **next scan**. Remember that "Next scan" is used to filter the results of a previous scan, based on the new value you specify, while "New Scan" or "First scan" will perform an entirely new search. + +In the results list: +- The **Address** column shows the memory address where the matching value was found. +- The **Value** column shows the current value at the address, updated in real-time. When the value changes, it is highlighted in red. +- The **Previous** column shows the value the last time we clicked the "Next scan" button. +- The **First** column shows the value at the time of the first scan. In our case, it should be "250" for all results. + +After the second scan, you should have a much smaller list of results. If you still have some results that are changing (highlighted in red) despite your coin count staying the same, you can perform additional scans without changing the value to filter them out, or remove them from the list manually. + +For this particular value, we should be able to narrow it down to 3 values pretty quickly with only a couple of scans, but it's not uncommon to have to repeat the operation (make the game change the value & scan for the new expected value) a few more times to get to a manageable results count. + +![Screenshot of the narrowed down results](../../images/ce-coinscan-results-narrowed.png)
+*The addresses you obtain can be different from the ones shown in this screenshot. This is expected, we will cover this some more later.* + +In the results list, you can double-click on an address to add it to the bottom panel. You can also select multiple results and click the little red arrow on the bottom-right of the results panel. The bottom panel is called the **address list**, and it's where you can keep track of the addresses you are interested in. + +Once you have added all of your narrowed-down results to the address list, we have to find out which of the three is the correct one. The most straightforward way to do this is to change the value and see if the coin count changes in the game. To do this, double-click on the value in the address list, enter a different value (e.g. `9999`), confirm, and check back in-game. + +In my case, modifying the second result seems to have no effect in-game, modifying the third one seems to instantly revert it back to 259, but modifying the first one does the trick. The coin count in the game is now 9999. + +![Screenshot of coin count with a value of 9999](../../images/sr-coincount-hacked.png)
+ +Congratulations! You have successfully found and modified a value in memory, and the game now believes you have 9999 coins. In fact, unless the game specifically guards against memory hacking (which Slime Rancher and most single-player-only games do not), it has no reason to get suspicious about our sudden fortune, and so it will let us use these coins however we want, no matter how ridiculously high we set the value. + +> [!WARNING] +> _Reminder:_ what we are doing here is fine because it's a single-player game, with no competitive aspect. **Using memory hacking for cheating in online games or to gain any kind of advantage (no matter how small) in a competitive space is wrong and will get you banned**. Always respect the rules of the games you play. Keep it fair and don't ruin the fun for others. **If you ignore this, I will do my best to shut down your project.** + +Editing values in the memory of your target is the core of memory hacking. Libraries like MindControl will help you automate the writing process, but you will still have to manually find the addresses. + +### Bonus training: hacking your health points + +As a bonus, try to find and modify your health points using the same technique. Here are a few hints to help you get started: +- You can lose health points by colliding with aggressive slimes in some areas of the game. +- Despite the game showing integral values for health points, remember that most games use floats to store values that show up in gauges or bars. +- Setting your health above 100 will revert it back to 100. This is a common behavior in games to prevent players from healing above the maximum health. Try setting it to a distinct value below 100 to see if it works. + +### Bonus bonus training: track a global statistic + +As a second bonus, try to find out the memory address that holds one of the following global stats (they don't reset when you start a new game): +- Total number of splorts sold +- Total money made from splorts +- Total number of chicken fed to slimes + +> [!TIP] +> These stats are tracked in the achievement menu, but the challenge is to find them without looking. You can look in the achievement menu once you've found them to verify that you have the correct numbers. + +A few hints: +- You probably haven't counted how many times you have done these things until now. In these cases, you can use the "Unknown initial value" scan type to get started. +- Once you have an initial scan, you can filter out unwanted addresses by increasing these numbers, and then using the "Increased value by..." scan type, specifying a value that represents by how much you have increased the counter since the last scan. For example, for the splorts sold counter, if you've sold 2 additional splorts since the last scan, specify 2. This will only keep addresses that have increased by that amount since the last scan. + +## Next part + +In the next section, we will point out a big issue with the technique we just used, and find out how to solve it using pointers. diff --git a/docs/guide/hacking-basics/stable-address.md b/docs/guide/hacking-basics/stable-address.md new file mode 100644 index 0000000..e2e5891 --- /dev/null +++ b/docs/guide/hacking-basics/stable-address.md @@ -0,0 +1,175 @@ +# Stable addresses + +In the previous section, we learned how pointers can help us pinpoint the location of a target value in memory, no matter how much the memory layout changes. We also learned that the pointer path has to start with a stable address if we want our hack to be reliable. This section will cover techniques to find stable addresses and follow pointer paths to get to the values we want to read or write. + +## What is a stable address? + +To explain stable addresses, we first need to cover modules. A module is a file that contains code and data that is loaded into memory when a program runs. In typical cases, you have an EXE (Executable) file, which contains the entry point of the program (first code executed), and then a bunch of DLL (Dynamic Link Library) files, which contain additional code that the EXE can reference. When a module is loaded into memory, it is assigned a base address, which is the memory address where the module starts. + +A stable address is the address of a value that is always located at the same offset, after the starting address to its parent module. The notation is usually `module_name + offset`, where `module_name` is the name of the module, and `offset` is a fixed value that represents the distance from the module's base address. For example, `Game.dll + 12C0` means "the address `12C0` bytes after the start of the `Game.dll` module". + +## Why are stable addresses important? + +Stable addresses are crucial for building memory hacking programs that are robust and reliable. If we can find a stable address that holds a pointer to a structure that we are interested in, we can follow it to get to the values we want to read or write, no matter how much the game changes things in memory. + +Without stable pointers, we would have to find addresses from scratch every time we start the game or change the game state in a way that moves things around in memory. + +## How to find stable pointer paths + +There are several ways to find pointer paths that start with a stable address. Fortunately, because this is a very common task in memory hacking, Cheat Engine provides a few tools to help us with that. This guide will only demonstrate some of the easiest techniques, but there are many ways to find them, that may or may not work depending on the target process. + +For the examples, we will use the coin count value from the Slime Rancher demo that we have found in a previous section. Start by attaching Cheat Engine to the game process and finding the address of the coin count value as described in the "[Finding values with Cheat Engine](./finding-values.md)" section. + +### Going backwards with the debugger + +Let's start with the most reliable way to find a stable address, but also one of the most tedious ones. Starting with the address of the coin count value, we will use the "Find out what writes to this address" feature of Cheat Engine to find the code that writes to the coin count value, and inspect it to see what the offset of the coin count member is within the structure that holds it. Then, we will look for a pointer to that structure, and so on, until we find a stable address. + +Start by right-clicking the coin count address in Cheat Engine from the address list, and selecting "Find out what writes to this address". Confirm that you want to attach the debugger. This will open the watcher window and add an entry whenever the game writes to the address. + +![Cheat Engine: Find out what writes to this address](../../images/ce-find-writes.png)
+ +Now, sell a plort in the game to cause it to write a new coin count value. You should see an entry appear in the watcher window. Click it, and then click the "More information" button on the right. This opens another window with extra info. + +![Cheat Engine: Finding code that writes to the coin count](../../images/ce-write-trace.png)
+ +So let's explain what we just did here. In the first window, we have a list of all instructions that have been writing a value at the coin count address. If the same instruction writes to that address multiple times, it will appear only once, but the counter on the leftmost column will show how many times it did. + +We didn't really explain what instructions are. Basically, they are the lines of code that the game is executing. The instructions are stored in memory, and the CPU reads them one by one to execute the program. + +The instructions that we are seeing in the memory of a process are in a format called **assembly language**. This is a low-level language, very close to the machine code that the CPU executes. Each instruction is represented by a mnemonic (a short word that represents the operation), followed by some parameters that tell the CPU what to do. For example, the `MOV` instruction moves a value from one place to another, and the `ADD` instruction adds two values together. + +![An example of assembly code](../../images/ce-assembly-example.png)
+*An example of a few assembly instructions, from the "Extra info" window of Cheat Engine.* + +Developers very rarely write in assembly language directly, but instead write code in a high-level language that is then transformed (through compilation or interpretation) into assembly. With memory hacking, we always have to work with assembly code, which is much harder to read and understand than the original code. + +> [!NOTE] +> This tutorial is not going to cover assembly language, because this is a much larger topic. We will only cover the basic elements required for our specific case. However, it's a good idea to learn more about it if you want to become proficient in memory hacking. You can find many resources online to learn assembly. + +Back to our example, the "Extra info" window shows us the assembly code that wrote to the coin count address. The exact instruction that wrote to the address is highlighted in red: + +```asm +mov [rax+00000080],ecx +``` + +It's a `MOV` instruction, which means that we are copying a value from one place to another. The `MOV` instruction takes two arguments: the destination and the source. In this case, the destination is `[rax+00000080]`, and the source is `ecx`. So that means it is copying the value of the `ecx` register to the address `[rax+00000080]`. If you don't know what registers are, to simplify, you can think of them as a limited set of variables that the CPU can use to store values temporarily. + +What's interesting here is that the instruction specifies an offset of `80` (in hexadecimal) bytes from the `rax` register. This typically means that `rax` stores the address of a structure that contains the coin count value at an offset of `+80` bytes. Looking at the bottom part of the "Extra info" window, we can see a dump of the values in CPU registers at the time the instruction was executed. Take note of the value of the `rax` register, as this is the address of the parent structure holding the coin count. In my case, it's `168698616C0`, but yours will be different. Once you've noted it down, and also noted down the offset (`+80`), you can close both windows. + +Now, let's take a look back at the pointer path example from the previous section: + +![An illustration showing a pointer path with a stable address](../../images/il-pointerchain.png)
+ +For now, we have only found the last offset of the pointer path. For the next step, we need to find memory values that hold the address of the parent structure. You know how to do that already: use the main window of Cheat Engine to scan for the value of the `rax` register. + +![Scanning for a pointer value in Cheat Engine](../../images/ce-scan-for-pointer.png)
+*Scanning for the value of the `rax` register in Cheat Engine. Replace the searched value with the value of `rax` you have noted before. Don't forget to pick `8 Bytes` in the value type dropdown (addresses are 8-byte integers in 64-bit programs), and to check the `Hex` checkbox to let you input a hexadecimal number.* + +This scan will give you a list of pointers that hold the address of the parent structure. If you look at the pointer path diagram from before, we need to keep finding offsets and parent structure addresses until we reach a stable address. So, now, we can repeat the steps we just did with the coin count address, but using the addresses we just found with the scan. + +For each scan result: +- Use "Find out what accesses this address" (or press Ctrl+F5) to find the instruction that reads the pointer. We have used "writes" before, but finding instructions that _read_ the pointer is more reliable after the first step. +- Mess around in the game until instructions appear in the watcher window. If it never does, give up on this pointer and try another one. +- Look at the assembly code to find the offset of the next structure in the same way we did before, and take note of the address of the parent structure, and the offset (sometimes there is no offset, in that case it's just `+0`). +- Scan for the parent structure address in Cheat Engine to find the next pointers. +- If the scan brings up stable addresses (they appear in green), you can stop there. If not, repeat the whole process until it does. + +Be careful with this process, as it's easy to get lost or to fall into a loop of pointers that reference each others. If you are stuck, try to go back a few steps and try another path. + +> [!TIP] +> Try doing at least a couple of steps of this process to make sure you understand how it works, but **don't feel like you have to go all the way to the end**. It's a very time-consuming process, and there are easier ways to find stable addresses (keep reading to find out!). + +![Scanning results with a stable address Cheat Engine](../../images/ce-scan-stable-pointer.png)
+*Repeating these steps will (hopefully) eventually lead you to a stable address. The last address in the list on this screenshot, highlighted in green, is stable.* + +Once you have a stable address to start with, click the "Add Address Manually" button on the right edge of the Cheat Engine main window, pick the target value type (in our case, `4 Bytes`), check "Pointer", and start filling in the fields. The bottommost field should be the stable address you found, and then the fields above it should be the offsets. Click "Add Offset" as needed to add more offset fields. Once you are done, the value shown next to the address field should be the value of the coin count. + +![Adding a pointer manually with Cheat Engine](../../images/ce-add-pointer.png)
+*An example pointer path added manually in Cheat Engine. Note that the `+80` offset we found earlier is the first one when read from top to bottom.* + +Now that we've done all this work, we finally have a stable pointer path that we can use to read or write the coin count value, no matter how much the game changes things in memory. Hopefully. In fact, there is no guarantee that the particular path we have found will work no matter what we do in the game. Maybe one of the structures we traverse is not guaranteed to hold a pointer to the next structure in all cases. Or maybe it does and everything works out just fine. To make sure, we have to test it out in the game, restart the game and test it again, and so on, to make sure that the path is indeed stable. If it's not, we have to go back in the process steps and find other paths. + +So all in all, this process is methodical and rather reliable, but it's also very time-consuming and requires a lot of manual work. It's a good idea to try it out at least once to understand how it works, but let's find out about another method that is much faster and easier. + +### Using the pointer scanner + +The pointer scanner is a tool in Cheat Engine that can automatically find pointer paths for you. It's not always reliable, but it's often a very efficient way to find stable addresses. + +So let's start over (restart the game if you have to) and find the coin count value again, as we did in the "[Finding values with Cheat Engine](./finding-values.md)" section. + +Start by right-clicking the address of the coin count value in Cheat Engine, and selecting "Pointer scan for this address". + +![The "Pointer scan for this address" option in Cheat Engine](../../images/ce-pointer-scan-open.png)
+ +This will open the pointer scanner option window. Most settings are fine as they are, but the most important one is the "max level" setting. This is the maximum number of pointers that the scanner will follow. The higher the number, the longer the scan will take, but the more likely it is to find a stable address. + +Generally, you should start with a low number for the max level, like 2 or 3, and increase it if you don't find any stable addresses. However, Slime Rancher is a Unity game, which is known to have a lot of pointer layers, so we are going to use a max level of 5. + +> [!NOTE] +> Increasing the max level will make the scan **exponentially** slower. To give you an idea, a max level of 7 may take a few seconds, a max level of 8 may take several minutes, and a max level of 9 may take days. + +In addition to the max level setting, I also recommend you check the "Pointers must end with specific offsets", and add the `+80` final offset that we have found earlier through the "Find out what writes to this address" method. This will make the scan faster and more reliable, as it will make sure pointer paths end with the offset we know is correct. + +![The pointer scanner options in Cheat Engine](../../images/ce-pointer-scan-options.png)
+ +Once you have set up the options, click "OK" to start the scan. Cheat Engine will ask you to save a `.ptr` file. Try to keep these organized, they are quite useful if you want to go back after making a mistake, or stop and then resume your hacking project later on. + +> [!TIP] +> A recommended way to keep your `.ptr` files organized is to create a folder for each game you are hacking, and then create a subfolder for each hack you are working on. Then, name your `.ptr` files with a descriptive name, a max level indication, and an index (we will see why in a moment). For this first one, I am going to use `D:\Hacking\SlimeRancherDemo\CoinCount\coincount-l5-001.ptr`. + +The scan may take a while, so be patient. When it's done, you will see a list of results. + +![The initial pointer scanner results in Cheat Engine](../../images/ce-pointer-scan-initial-results.png)
+ +This window shows all the pointer paths that the scanner has found. The number of results is displayed above the table (in my screenshot, it's 984, but yours will probably be slightly different). The "Points to" column shows the final address and its value. These should all have a value corresponding to your current coin count for now. + +> [!NOTE] +> The values shown in the "Points to" column are **not** refreshed in real time. They are refreshed whenever you select the row. + +So, in the current state of the game, all of these stable pointer paths lead to our coin count. But that doesn't mean they will always do. We have to test them out to make sure they are indeed stable. The best way to do that is to mess with the game state, and perform an additional scan to filter out results that no longer point to our coin count. + +The first thing we can do is to go back to the menu and start a new game. This should shuffle things around in memory, but the paths that are stable should still point to the coin count. When you've done that, go back to the pointer scan results window, and in the "Pointer scanner" menu, select "Rescan memory - Removes pointers not pointing to the right address". + +The rescan window that opens allows you to specify what you want to filter out. The easiest scan we can do is to pick "Value to find" and enter the expected value we should be pointing to. In our case, because we just started a new game, we are looking for the value `250` (the starting coin count). + +![The pointer scanner rescan options in Cheat Engine](../../images/ce-pointer-scan-rescan.png)
+ +Click "OK" to start the rescan. This will effectively filter out paths that no longer point to an address with a value of `250`. + +> [!TIP] +> Every time you rescan, you are asked to save a new `.ptr` file. As stated before, this is useful to go back to a previous state. Following the convention from before, I am going to save this one as `D:\Hacking\SlimeRancherDemo\CoinCount\coincount-l5-002.ptr`. + +When the rescan is done, your list should have been trimmed significantly. + +![The trimmed pointer scanner rescan results in Cheat Engine](../../images/ce-pointer-scan-results-2.png)
+*We now have 257 results, down from 984. Your numbers may be different.* + +We can trim the list further by repeating the process of messing with the game state, and then rescanning. For example, you can sell a plort to change the coin count, and then rescan for the new value. This will filter out paths that don't point to the new coin count value. + +One of the best ways to mess with game state is to close the game and start it back up. This will shuffle things around in memory a lot, which means that paths that still hold after multiple reboots are very likely to be reliable. + +> [!NOTE] +> After you restart the game, don't forget to **re-attach the process in Cheat Engine**! You don't have to close any window, everything will update automatically. + +After a few iterations of this process, you should have a list of stable pointer paths that you can use to read or write the coin count value. + +There is no way to know for sure if any of these will always hold no matter what you do in the game. Just keep iterating until you are confident enough that the paths are stable. + +If all of your results get filtered out, it can mean one of the following: +- You have made a mistake in the rescan options. Make sure you are looking for the right value, and that the process is attached. You can always load a previous `.ptr` file to go back to a previous state without having to restart from scratch. +- There are no stable pointer paths with the specified max level. Start all over again with an increased max level (in the Slime Rancher demo, you should be able to find stable paths with a max level of 5). +- There are no stable pointer paths for your value at all. This can happen for example when one of the target structures may be at various indexes in a list, or if the game has cheat protections. + +![The final pointer scanner results in Cheat Engine](../../images/ce-pointer-scan-results-final.png)
+*In this case, we quickly reach a list of 9 paths that are seemingly stable.* + +When you are confident in your results, pick any of the paths that you have found (it's a good idea to pick shorter ones, but in our case, they all have 5 offsets). Double-click any result to add the pointer path to the address list in the main window of Cheat Engine. + +You now have a stable way to read or write the coin count value, no matter how much the game changes things in memory. Congratulations! + +> [!NOTE] +> Cheat Engine allows you to save your address list and more as a `.ct` file. This is useful to keep your progress, and to share it with others. You can find the save option in the "File" menu. + +## Next part + +We have now covered the basics of finding stable addresses and following pointer paths to get to the values we want to read or write. In the next section, we will learn another key concept in memory hacking: modifying the game code. diff --git a/docs/guide/hacking-basics/unstable-address.md b/docs/guide/hacking-basics/unstable-address.md new file mode 100644 index 0000000..07d2559 --- /dev/null +++ b/docs/guide/hacking-basics/unstable-address.md @@ -0,0 +1,85 @@ +# Unstable addresses + +In the previous section, we learned how to find a memory address that stores a specific value. We found the address that stores the coin count in the Slime Rancher demo, and we changed it to a different value. This is a very basic form of memory hacking, but it's already quite powerful. However, there is a catch. + +## The issue + +With the address of the coin count still in the address list in Cheat Engine, try to go back to the main menu of the game, and then start a new game. You will notice that the value at this address doesn't change. It's stuck with the same old value, and won't reflect the coin count in our new game. + +Worse, if you exit the demo (close it entirely) and then start it back up and load into a new game (don't forget to re-attach to the game in Cheat Engine every time you do that!), the address will either be invalid (the value will show up as `??`), or it will hold a value that doesn't have anything to do with the coin count. + +If you try to find the address again using the same technique we used in the previous section, you will notice that it's not the same as before. It's a different address. + +**Why is that?** + +Programs manipulate structures in memory all the time. They allocate memory for variables, use it, and then deallocate it. They move things around, they change the size of structures, they create new structures, and they destroy them. This is all part of the normal operation of a program. + +When a program does this, the memory addresses that we found before can be discarded or reused for other purposes. The address that holds the coin count is now different because the memory of the game has been shuffled around when we started a new game or booted up the game again. This is why we call these addresses **unstable**. + +To solve this issue, we need to understand two key concepts: **structures** and **pointers**. + +## Understanding structures + +A structure is a set of variables that are grouped together under a single name. Each variable in the structure is called a member. If you think about it in terms of object-oriented programming, a structure is like a class, and a member is like a field. Members are always organized in the same order across different structures of the same type, and will always be of the same length. + +Let's take some examples and define the following two structures (these are simplified examples, not actual structures in Slime Rancher or any other game): + +*The `Player` structure* + +| Field | Type | Offset | +|--------------|------------|--------| +| healthPoints | float | 0x08 | +| weaponHeld | pointer | 0x10 | +| coinCount | 4-byte int | 0x18 | + +*The `Weapon` structure* + +| Field | Type | Offset | +|-----------|------------|--------| +| cost | 4-byte int | 0x08 | +| damage | 4-byte int | 0x0C | +| swingTime | float | 0x10 | + +In these tables, the `Offset` column tells us where the field is located within the structure. For example, if we know the address of a `Player` structure, we can find the `coinCount` field by adding `0x18` (hexadecimal notation for the number `24`) to the address, no matter where the `Player` structure itself is located. Doing a little bit of math, if we know our `Player` structure starts at the address `182F359202C`, the `coinCount` member will be at the address `182F3592034`. + +Now, you'll notice that the `weaponHeld` member is a pointer, which is the second concept we need to understand to solve our issue. + +## Understanding pointers + +### What is a pointer? + +A pointer is simply a variable that holds the address of another variable. It's like a note that says "the value you are looking for is at this address". In our examples, the `weaponHeld` member of the `Player` structure is a pointer to a `Weapon` structure. This means that the `weaponHeld` member holds the address of a `Weapon` structure, and not the `Weapon` structure itself. + +Looking at the offsets and value types, we can see that a `Weapon` structure is 24 bytes long. Regardless of that, the `Player` structure can have a `weaponHeld` member that is always 8 bytes long, because it only holds the address of the `Weapon` structure. The `Weapon` structure itself can be located anywhere else in memory. + +### How do pointers solve our issue? + +Let's take a look back at our "weapon held" example. Say we want to track our current weapon damage. We have found that the address of the `Weapon` structure for the weapon we are currently holding is `182F3593120`. Using the offset, we know that the damage value is at `182F359312C`. Now imagine that, in game, we equip a different weapon. + +What happens in that case is that the game will update the value of the `weaponHeld` member in our `Player` structure to hold the address of the new weapon structure (this can also be called "pointing to" the new weapon structure). The previously held `Weapon` structure will not be touched. + +When we look at our `182F359312C` address, we still see the same damage value as before, because it still is a field within the old `Weapon` structure. Even worse, if it doesn't need the old weapon anymore, maybe the game will discard the old structure, and the address may become invalid, or be reused to hold different data. + +![An illustration describing the scenario when observing a fixed address](../../images/il-pointers-firstscenario.gif)
+*We observe the same address, regardless of the change, so we still get the same value of 5 after the weapon changes.* + +Now, let's say that instead of having a fixed `182F359312C` address that we watch, we first read the address of the `weaponHeld` member in the `Player` structure, to know where the weapon structure is stored. Then, we can add the `damage` member offset (`+C`) to that address, and we will always get the correct damage value, no matter how many times the game moves things around in memory. + +![An illustration describing the scenario when using pointers](../../images/il-pointers-secondscenario.gif)
+*We observe the pointer to the equipped weapon, and follow it with an added +0C offset to get to the damage value. This time, we get the correct value of 17 after the weapon changes.* + +The thing is, the `Player` structure itself can move around, especially if we do things like going back to the menu and loading back in, or even worse, close the game and start it back again. So, following the same logic, we have to find a pointer within a different structure that holds the address of the `Player` structure. Maybe there's a `GameManager` structure that has a `player` pointer member. And then maybe there's a `Game` structure that has a `gameManager` pointer member. And maybe that `GameManager` structure is always located at the same address, relative to a module (more on that later). + +If we can figure out this whole chain, we can follow the pointer path: +> `Game` structure address + `gameManager` member offset → `GameManager` structure address + `player` member offset → `Player` structure address + `weaponHeld` member offset → weapon structure address + `damage` member offset + +And that pointer path would always be stable, meaning that no matter how much the game moves things around in memory, and no matter how many times we restart the game, we can always get to the `damage` member of the weapon we are holding. + +![An illustration showing a pointer path with a stable address](../../images/il-pointerchain.png)
+*We observe the stable address `game.exe + 1FC08` (we'll learn more about these in the next section) that starts a sequence of pointers, that we follow with known offsets added each time, to finally get to the damage value of the weapon held by the player.* + +The key to these pointer paths is to find the first address in the chain that never moves around. This is called a **stable address**. + +## Next part + +In the next section, we will learn techniques to find stable addresses and follow pointer paths to get to the values we want to read or write. This will allow us to build memory hacking programs that are robust and reliable, even when the game moves things around in memory. diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md new file mode 100644 index 0000000..140f4ed --- /dev/null +++ b/docs/guide/introduction.md @@ -0,0 +1,26 @@ +# Introduction + +This guide is intended to demonstrate how to use the `MindControl` .net library to interact with the memory of a running process. However, to make sure your understanding of the basics of memory hacking is aligned with the library's design, we will first cover some fundamental concepts. If you are new to memory hacking, this guide will help you get started on your journey. If you are already familiar with process hacking, you can skip ahead to the [Project setup](./project-setup/creating-mc-project.md) section. + +## What is memory hacking and what can we do with it? + +Memory hacking is basically manipulating the internal values used by a process that is running on your system, while it's running. + +Every program that runs on your system is loaded into memory. This memory stores everything the program needs to run, including the values of variables, the code that is being executed, and the data that is being processed. By changing these values, you can manipulate the behavior of the program. + +Usually, each program minds its own business and has a separate set of memory. However, using system functions, you can read and write to the memory of any process running on your system. A memory hacking program will make use of these functions to access the memory of its target program. + +This technique is often used in gaming, to build all sorts of tools, such as game trainers, cheats, bots, overlays, and automation tools. Mods can also be built using memory hacking, although they typically also require other skills. It can also be used in general-purpose software, for debugging, reverse engineering, security testing, and more. + +> [!WARNING] +> Just a reminder before you keep going: **using memory hacking for cheating in online games or to gain any kind of advantage (no matter how small) in a competitive space is wrong and will get you banned**. Always respect the rules of the games you play. Keep it fair and don't ruin the fun for others. **If you ignore this, I will do my best to shut down your project.** + +## What MindControl is about + +MindControl is a .net library that provides a set of tools to interact with the memory of a running process. It allows you to easily build memory hacking programs that read and write values, search for patterns, inject code, and more. It's designed to be simple to use, reliable, and efficient. + +As stated before, operating systems like Windows provide functions to do that already. However, these functions are low-level, complex to understand and cumbersome to use. MindControl provides an additional layer on top of that, that considerably simplifies the process. + +## Next step + +The next section will be about gathering everything you need to follow this guide. \ No newline at end of file diff --git a/docs/guide/mcfeatures/allocations.md b/docs/guide/mcfeatures/allocations.md new file mode 100644 index 0000000..ca7752b --- /dev/null +++ b/docs/guide/mcfeatures/allocations.md @@ -0,0 +1,107 @@ +# Storing data in memory + +This section explains how to allocate memory and store data in the memory of the target process using MindControl. + +## What is a memory allocation? + +A memory allocation is a block of memory that is reserved in the target process's address space. This memory space can be used to store data, such as structures, arrays, strings, pictures, or even code. + +For example, if you want to replace a texture in a game, you have to: +1. Allocate memory in the target process to store the new texture data. +2. Write the texture data to the allocated memory. +3. Overwrite pointers to use the new texture data from the allocated memory instead of the normal one. + +MindControl provides three features for working with memory allocations: allocations, reservations, and storage. + +## Storage + +For most use cases, you don't need to allocate memory manually. Instead, you can use the `ProcessMemory` class to store data in the target process's memory. This is done using the `Store` method, which takes a value and returns a `MemoryReservation` object that represents a reservation on allocated memory (more on that later). You can then get the address of the reservation and reference it wherever you need to. + +```csharp +// Store an integer value in the target process memory. This could be any other data type that you can write. +var reservationResult = processMemory.Store(42); +if (reservationResult.IsSuccess) +{ + // We can then use the Address property of the MemoryReservation + // In this example, we read the value back from the target process memory, but you would typically write a pointer + // to this address somewhere so that the process uses it. + MemoryReservation reservation = reservationResult.Value; + int value = processMemory.Read(reservation.Address).ValueOrDefault(); + Console.WriteLine($"Stored value: {value}"); // Output: Stored value: 42 +} +``` + +When calling `Store`, under the hood, MindControl will: +- Allocate a chunk of memory in the target process that is large enough to hold the data you want to store, but usually bigger than that for various reasons +- Keep track of the allocated memory in a `MemoryAllocation` object to maybe reuse it later +- Reserve a portion of the `MemoryAllocation` just big enough to hold the data you want to store, essentially creating a `MemoryReservation` object +- Write the data at the address of that reservation +- Return the `MemoryReservation` object + +If we call `Store` again with some data that is small enough to fit in the same `MemoryAllocation`, MindControl will reuse the same allocation, and create a new `MemoryReservation` on it, for your new data. This is done to avoid unnecessary memory allocations and to optimize memory usage. + +```csharp +var reservation1 = processMemory.Store(42).Value; +var reservation2 = processMemory.Store(64).Value; +// Only one memory allocation is issued, but two different reservations are created. +// Usually, you don't need to worry about this. +``` + +The advantage of using `Store` is that it abstracts away the details of memory allocation and management, allowing you to focus on the data you want to store rather than the underlying memory operations. You don't have to worry about where the memory is allocated, how much space is reserved, about the system page size, data alignment, or even about accidentally overwriting the memory you allocated. + +> [!NOTE] +> Disposing the `MemoryReservation` object will automatically free the memory reserved for your data, so that it can be reused to store other data later. If your program dynamically stores more and more data, you have to make sure to dispose of the `MemoryReservation` objects when you no longer need them, to avoid memory leaks in the target process. + +The two other features, allocation and reservation, are more low-level and give you more control over the memory management process. They're usually not needed, but there are cases where you might want to use them. + +## Allocations + +If you need to allocate memory manually, you can use the `Allocate` method of the `ProcessMemory` class. This method allows you to allocate a block of memory in the target process's address space. This method returns a `MemoryAllocation` object that represents the allocated memory. + +```csharp +// Allocate a block of memory of at least 1024 bytes in the target process, to store data (not code) +MemoryAllocation allocation = processMemory.Allocate(1024, forExecutableCode: false).Result; +// The actual allocation size may be larger than 1024 bytes, depending on the system page size and other factors. +``` + +Note that the second parameter, `forExecutableCode`, specifies whether the allocated memory should be executable or not. If you plan to write code to this memory, you should set this parameter to `true`. Otherwise, you can set it to `false` to avoid performance overhead and potential security risks. + +There are also two optional parameters that you can use to provide memory range limits for the allocation, and/or to specify that the memory allocation should be made as close as possible to a specific address. This is most useful when performing code injection and other advanced memory manipulation techniques. + +> [!NOTE] +> Like the reservations, disposing `MemoryAllocation` instances will free the memory allocation in the target process. If your program creates allocations dynamically, make sure to dispose of them when you no longer need them, to avoid memory leaks in the target process. + +With the returned `MemoryAllocation` object, you can perform reservations, which is the topic of the next section. + +## Reservations + +If an allocation is a physical block of memory in the target process, a reservation is a logical portion of that allocation that you explicitly mark as used. + +Basically, reservations are another layer on top of allocations that allow you to manage your allocations more easily. When you make a reservation, you are locking in a portion of the allocated memory, and that makes sure that future reservations will not overlap with it. + +There are two ways to create a reservation: using the `ReserveRange` method of the `MemoryAllocation` class, or using the `Reserve` method directly on the `ProcessMemory` class. + +### Using a MemoryAllocation + +```csharp +MemoryAllocation allocation = processMemory.Allocate(1024, forExecutableCode: false).Result; +// Reserve 256 bytes of that allocation +MemoryReservation reservation = allocation.ReserveRange(256).Result; +// You can now use the reservation to write data +processMemory.Write(reservation.Address, 42); +``` + +### Using ProcessMemory + +The `Reserve` method is a more convenient way to create a reservation without having to manage the allocation yourself. It will browse existing allocations to find one that satisfies the required size, create one if none exists, and then make a reservation in it. + +```csharp +// Reserve 256 bytes of memory in the target process, without manually creating a new allocation +MemoryReservation reservation = processMemory.Reserve(256, requireExecutable: false).Result; +// You can now use the reservation to write data +processMemory.Write(reservation.Address, 42); +``` + +## Conclusion + +To recapitulate, in most cases, you can use the `Store` method to store data in the target process's memory without worrying about allocations and reservations. If you need more control over memory management, you can use the `Reserve` method to create reservations directly, or, if you need even more control, you can use `Allocate` to manage allocations yourself. diff --git a/docs/guide/mcfeatures/attaching.md b/docs/guide/mcfeatures/attaching.md new file mode 100644 index 0000000..aa00b19 --- /dev/null +++ b/docs/guide/mcfeatures/attaching.md @@ -0,0 +1,75 @@ +# Attaching to a process + +Manipulating the memory of a running process with MindControl requires attaching to that process to get a `ProcessMemory` instance. In this section, we will cover how to attach to a process using the `MindControl` library. + +## The Process Tracker + +In most cases, the best way to attach to a process is to use the `ProcessTracker` class. This class has a very simple API: you specify the name of the process you want to attach to, and then you can get a `ProcessMemory` instance whenever you want to perform a memory manipulation operation. If the process is not running at the time of the call, it will return `null`. + +Here is an example of how to use a `ProcessTracker` in a class: + +```csharp +public class SlimeRancherProcess +{ + // Keep an instance of the ProcessTracker in your class and give it the name of the target process. + private readonly ProcessTracker _processTracker = new("SlimeRancher"); + + public int? GetCoinCount() + { + // Attempt to get the ProcessMemory instance for the target process + // If the process is not running, this will return null + var process = _processTracker.GetProcessMemory(); + if (process == null) + return null; + + // Use the ProcessMemory instance to read a value from the target process + var coinCountResult = process.Read("UnityPlayer.dll+0168EEA0,8,100,28,20,80"); + return coinCountResult.ValueOr(0); + } +} +``` + +The `ProcessTracker` class allows you to easily attach to your target process, without having to worry about: +- The order in which the processes are started: if your program starts first, `GetProcessMemory` will just return `null` until the target process is started. +- The process being closed: if the target process is closed, `GetProcessMemory` will return `null` after the process is closed. +- The process being restarted: if the target process is restarted, `GetProcessMemory` will return a new `ProcessMemory` instance for the new process. You don't have to care about this. + +## Attaching directly through the `ProcessMemory` class + +There are cases where you might want to attach to a process directly without using the `ProcessTracker`. For example: +- You know the process ID of the target process and want to attach to it directly. +- You are building a "one-shot" tool that attaches to a process, performs some operations, and then exits. +- You want to attach to multiple processes with the same name. + +For these cases, you can use the `ProcessMemory` class directly. Here are examples of how to do this: + +### Attaching to a process by name + +```csharp +using MindControl; + +var result = ProcessMemory.OpenProcessByName("MyTargetProcess"); // Replace with the actual process name +result.ThrowOnFailure(); // Throws an exception if the process could not be opened +var processMemory = result.Value; +``` + +### Attaching to a process by PID (process ID) + +```csharp +using MindControl; + +var result = ProcessMemory.OpenProcessById(1234); // Replace with the actual process ID +result.ThrowOnFailure(); // Throws an exception if the process could not be opened +var processMemory = result.Value; +``` + +### Attaching to a process with a System.Diagnostics.Process instance + +```csharp +using MindControl; + +var process = System.Diagnostics.Process.GetProcessById(1234); // You can use other methods to get the Process instance +var result = ProcessMemory.OpenProcess(process); +result.ThrowOnFailure(); // Throws an exception if the process could not be opened +var processMemory = result.Value; +``` \ No newline at end of file diff --git a/docs/guide/mcfeatures/code-manipulation.md b/docs/guide/mcfeatures/code-manipulation.md new file mode 100644 index 0000000..2f3b809 --- /dev/null +++ b/docs/guide/mcfeatures/code-manipulation.md @@ -0,0 +1,169 @@ +# Manipulating code + +This section is a bit more advanced and will explain how to manipulate code in the target process using MindControl. + +> [!NOTE] +> This section requires the `MindControl.Code` package to be installed in addition to the `MindControl` package. + +## What is code manipulation? + +Code manipulation refers to the ability to modify the executable code of a running process. This can include removing or changing instructions, injecting new code, or redirecting execution flow. Code manipulation is often used in game hacking, reverse engineering, and debugging scenarios. + +Executable code is stored in the process memory, just like any other data, under the form of instruction bytes called opcodes. These opcodes are executed by the CPU to perform various operations, such as arithmetic calculations, memory access, and control flow changes (jumping to a different instruction, often depending on various conditions). + +Manipulating code can be challenging, because messing up a single bit in an instruction often leads to crashes or unexpected behavior. Injecting new code without modifying the existing code is especially difficult, because you cannot just insert new instructions in the middle of existing code without breaking the flow of execution. + +## Opcodes + +Opcodes are the machine-level instructions that the CPU executes. Each opcode corresponds to a specific operation, usually very basic, such as adding two numbers, jumping to a different instruction, or calling a function. Opcodes are represented as byte sequences in memory, and they can vary depending on the CPU architecture (e.g., x86, x64). + +When using a memory hacking tool such as Cheat Engine, you can view the disassembled code, which shows the opcodes in a human-readable format (assembly language). This allows you to see what the code is doing and how it is structured, but it is still very complex to understand, because meaningful operations (like "fire a bullet") are made of thousands of opcodes, each performing a basic operation that has no obvious meaning by itself. + +Here are some examples of common opcodes in x64 architecture: +```assembly +mov eax, 1 ; B8 01 00 00 00 - Move the value 1 into the EAX register. +add eax, 2 ; 05 02 00 00 00 - Add the value 2 to the EAX register. +jmp 0x12345678 ; (Bytes depend on multiple factors) - Jump to the instruction at address 0x12345678. +call 0x12345678 ; (Bytes depend on multiple factors) - Call the function at address 0x12345678 +nop ; 90 - No operation, does nothing. +``` + +Note that the opcodes are not always the same length, and some operations can take different forms depending on the operands used. For example, the `jmp` and `call` instructions can have different byte sequences depending on whether they use absolute addresses, relative offsets, or other addressing modes. + +> [!NOTE] +> When diving into code manipulation, it is very advisable to learn the basics of assembly language. This is outside the scope of this guide, but there are many resources available online to help you get started. + +## Removing code instructions + +The easiest code manipulation operation is to remove code instructions. This can be done by overwriting the instructions with `NOP` (No Operation) instructions, which effectively make the code do nothing. + +Fortunately, `NOP` instructions are only one byte long, so you can replace any instruction with a number of `NOP` instructions without changing the size of the code. For example, removing a `mov eax, 1` instruction would be as simple as writing 5 bytes of `0x90` (the opcode for `NOP`) at the address of the instruction. + +MindControl provides easy-to-use methods to remove code instructions. The `DisableCodeAt` method disables a number of instructions at a specific address by overwriting them with `NOP` instructions. + +```csharp +// Disable 5 instructions, starting at the address "mygame.exe+0168EEA0" +CodeChange codeRemoval = processMemory.DisableCodeAt("mygame.exe+0168EEA0", 5); + +// Disposing the CodeChange object will restore the original code +codeRemoval.Dispose(); +``` + +> [!NOTE] +> It is important to consider that removing code instructions can lead to unexpected behavior, especially if the removed instruction is part of a larger control flow structure (like a loop or a conditional statement). If you don't know what the removed instructions do, chances are you will cause a crash. + +## Injecting code + +Injecting code is a more advanced operation that allows you to add new instructions to the target process. This can be used to implement custom functionalities or to modify existing behavior. + +Because we cannot just insert new instructions in the middle of existing code and shift everything around, this is usually done through a hook. A hook is a technique that intercepts the execution flow of the target process and redirects it to your custom code, usually redirecting it back to the original code afterward. + +The steps are the following: +1. Allocate executable memory in the target process to store the new code. Ideally, the code is located near the original code, to optimize performance. +2. Write the new code to the allocated memory. +3. Overwrite the original code at the address of the target instruction with a jump instruction that redirects execution to the new code. + +Typically, the code you write at step 2 will end with a jump instruction that redirects execution back to the original code, so that the original code can continue executing after your custom code has run. This is often referred to as a "trampoline". (So now you know how to build trampoline hooks.) + +If you want to inject code without replacing functionality, the code you write at step 2 may start with whatever instructions end up being replaced by the jump instruction at step 3. + +There is a big issue with that though. The code you write will often use registers and set CPU flags, meaning that, when you redirect execution back to the original code, the state of the CPU will not be what the original code expects. This often leads to crashes or unexpected behavior. To protect against this, you need to save the state of the CPU before executing your custom code, and restore it afterward. This is often done by pushing the registers onto the stack at the start of your custom code, and popping them back at the end. + +We won't go into the details of how to write assembly code for this, but we will see how to do it using MindControl. + +Whatever you want to achieve, MindControl provides three ways to inject code. + +### Using InsertCodeAt + +The `InsertCodeAt` method of `ProcessMemory` allows you to inject code at a specific instruction in the target process. It takes either the address of a pointer path to the address of an instruction, and the code to inject. In this variant, the code is going to be executed before the original instruction, and the original instruction will be executed afterward. No instructions are removed, the original code is preserved entirely. + +The code you provide is either a byte array, or a `Iced.Intel.Assembler` object that contains some code ready to be assembled. The `Iced.Intel.Assembler` class is part of the Iced project, which is a library for disassembling and assembling x86/x64 code. You can use it to line up assembly code in your .net project that you can then inject through MindControl. + +```csharp +// Create an assembler and write some code to it +var assembler = new Assembler(64); +assembler.mov(rcx, value); +// ... + +// Insert the code at the address "mygame.exe+0168EEA0" +CodeChange codeInjection = processMemory.InsertCodeAt("mygame.exe+0168EEA0", assembler).Value; + +// Disposing the CodeChange object restores the original code and frees the memory reservation where code was written +codeInjection.Dispose(); +``` + +### Using ReplaceCodeAt + +Similarly, the `ReplaceCodeAt` method allows you to replace one or more instructions with your own code. This is useful when you want to modify the behavior of existing code. + +The differences with `InsertCodeAt` are: +- You can specify the number of instructions to replace, and the code you provide will be executed **instead of** the original instructions. +- If your code is shorter or equal in size to the original instructions, the original code will simply be overwritten. If your code is longer, a hook will be performed. You don't have to worry about that. + +```csharp +// Create an assembler and write some code to it +var assembler = new Assembler(64); +assembler.mov(rcx, value); +// ... + +// Replace 3 instructions, starting at the instruction at address "mygame.exe+0168EEA0", with the code we just prepared +CodeChange codeInjection = processMemory.ReplaceCodeAt("mygame.exe+0168EEA0", 3, assembler).Value; + +// Disposing the CodeChange object restores the original code and frees the memory reservation where code was written +// (in cases where a hook was necessary). +codeInjection.Dispose(); +``` + +### Using Hook + +Finally, the `Hook` method allows you to specify what kind of hook to perform, through a `HookOptions` object. This method provides slightly more control over the hook, but it's almost always possible to achieve the same result using either `InsertCodeAt` or `ReplaceCodeAt`. + +```csharp +// Create an assembler and write some code to it +var assembler = new Assembler(64); +assembler.mov(rcx, value); +// ... + +// Create hook options to specify the type of hook to perform +HookOptions hookOptions = new HookOptions(HookExecutionMode.ExecuteInjectedCodeFirst); + +// Hook the instruction at address "mygame.exe+0168EEA0" with the code we just prepared +CodeChange codeInjection = processMemory.Hook("mygame.exe+0168EEA0", assembler, hookOptions).Value; + +// Disposing the CodeChange object restores the original code and frees the memory reservation where code was written +codeInjection.Dispose(); +``` + +> [!NOTE] +> It's generally discouraged to use the `Hook` method directly, as it is more complex and less intuitive than the other two methods. The `InsertCodeAt` and `ReplaceCodeAt` methods are usually better and easier to read. + +### Code isolation + +As we have previously touched, injecting code can lead to unexpected behavior if the injected code does not properly handle the CPU state. To make sure that your injected code does not interfere with the original code, you have to save and restore the CPU registers and flags before and after executing your custom code. + +You can do this manually by pushing the registers onto the stack at the start of your custom code, and popping them back at the end. However, MindControl provides a more convenient way to do this through an additional parameter in both `InsertCodeAt` and `ReplaceCodeAt`. + +This parameter is an array of `HookRegister`, an enumeration that you can use to list the registers that your code uses. When performing the code manipulation, MindControl will automatically save and restore the state of these registers. + +Additionally, if your injected code is very complex or if you want to make sure that it does not interfere with the original code, you can use one of the pre-made arrays available through the `HookRegisters` static class. For example, `HookRegisters.AllCommonRegisters` is a pre-made array of all commonly used registers, and using it in a code manipulation operation will almost always guarantee that your code does not interfere with the original code. + +In performance-critical scenarios, you should try to list only the registers that your code actually uses, to avoid the overhead of saving and restoring unnecessary registers. + +#### Example using a few registers + +```csharp +// Save and restore the state of the RCX and RBX registers, and the CPU flags +CodeChange codeInjection = processMemory.InsertCodeAt("mygame.exe+0168EEA0", assembler, + HookRegister.RcxEcx, HookRegister.RbxEbx, HookRegister.Flags).Value; +``` + +#### Example using all common registers + +```csharp +// Using all common registers is a catch-all solution, but runs slower +CodeChange codeInjection = processMemory.InsertCodeAt("mygame.exe+0168EEA0", assembler, + HookRegisters.AllCommonRegisters).Value; +``` + +> [!NOTE] +> In the `HookRegister` enumeration, x64 and x86 versions of the same register are grouped together, because MindControl is not compiled against a specific architecture. For example, `HookRegister.RcxEcx` refers to the x64 `RCX` register if your target process is x64, or the x86 `ECX` register if your target process is x86. \ No newline at end of file diff --git a/docs/guide/mcfeatures/dll-injection.md b/docs/guide/mcfeatures/dll-injection.md new file mode 100644 index 0000000..76b40f7 --- /dev/null +++ b/docs/guide/mcfeatures/dll-injection.md @@ -0,0 +1,45 @@ +# Injecting a DLL + +A common technique in process hacking is to inject a DLL into the target process. This allows you to run custom code within the context of the target process, which can be useful for various purposes, such as modifying the behavior of the application, hooking functions, or even creating a user interface. + +## Creating a DLL for injection + +To create a DLL for injection, you can write a simple C++ program with an `APIENTRY` function that will be called when the DLL is loaded. Here's a basic example: + +```c++ +#include +#include + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + // This code will run when the DLL is injected into the target process + // In this example, we will just show a messagebox, but you can replace this with your own code + MessageBoxA(NULL, "Injected library attached", "DLL Injection", MB_OK | MB_ICONINFORMATION); + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + + return TRUE; +} +``` + +Compile this code into a DLL using your preferred C++ compiler. Make sure to set the output type to "Dynamic Link Library" (DLL), and use the appropriate bitness (x86 or x64) that matches the target process you want to inject into. Then, you can add this DLL to your MindControl project. + +> [!NOTE] +> When referencing the DLL in your MindControl project, just add it as a content file, and not a reference. Make sure that the DLL is copied to the output directory of your project. + +## Injecting the DLL using MindControl + +Once you have your DLL ready, you can inject it into the target process using the `ProcessMemory.InjectLibrary` method. + +```csharp +// Pay attention to the result of the method call, as there are many reasons why the injection might fail +processMemory.InjectLibrary("path_to_your_dll.dll").ThrowIfFailure(); +``` + +This method will attempt to inject the specified DLL into the target process. If the injection is successful, the code in the `DllMain` function of your DLL will be executed before the method returns. For example, if you used the code provided above, you should see the message "Injected library attached" in a message box when the DLL is injected successfully. \ No newline at end of file diff --git a/docs/guide/mcfeatures/freezing.md b/docs/guide/mcfeatures/freezing.md new file mode 100644 index 0000000..de86a88 --- /dev/null +++ b/docs/guide/mcfeatures/freezing.md @@ -0,0 +1,20 @@ +# Freezing values in memory + +This section covers a technique often called "freezing". The idea is to lock the value of a variable in memory so that it does not change, even as the game tries to modify it. This can be useful for debugging or, let's be real, to make cheats in games. + +In MindControl, this is done through memory anchors. An anchor is like a persisting reference to a specific variable in memory, of a specific type, and it allows you to perform various operations on that specific target. + +```csharp +var anchor = processMemory.GetAnchor(); + +// Freeze that variable to the value 1234567 +var freezer = anchor.Freeze(1234567); + +// To unfreeze the variable, dispose the freezer +freezer.Dispose(); +``` + +When you freeze a variable, MindControl will continuously write the specified value to the target memory location, effectively locking it in place. This means that even if the game changes the value, it will be very quickly overwritten with the frozen value you chose. + +> [!NOTE] +> Freezing is resource-intensive, as it requires continuous writes to memory. Be careful not to overuse it, and make sure you dispose freezers that you no longer need. diff --git a/docs/guide/mcfeatures/functions.md b/docs/guide/mcfeatures/functions.md new file mode 100644 index 0000000..fefc6a7 --- /dev/null +++ b/docs/guide/mcfeatures/functions.md @@ -0,0 +1,108 @@ +# Executing remote functions + +This section covers how to execute functions in your target process. This can allow you to perform actions in the target process from your program, or to call your own functions after injecting a DLL into the target process. + +You can execute functions with the method `RunThread`. There are two main ways to call this method: + +## With an address or pointer path + +You can call a function by providing its address or a pointer path to it. This is useful when you know the exact location of the function in memory. + +```csharp +var result = processMemory.RunThread("UnityPlayer.dll+0168EEA0,8,100,28,20,80"); +result.ThrowOnFailure(); // Throws an exception if the function could not be executed + +// Wait for the function to complete, with a timeout of 10 seconds +result.Value.WaitForCompletion(TimeSpan.FromSeconds(10)); + +// Dispose the result when done to ensure resources are released +result.Dispose(); +``` + +## With a module and function name + +You can also call a function by providing the module name and the function name. This is useful when you want to call a function in a specific module without needing to know its address. This uses the module's export table to find the function, and thus requires the module to explicitly export the function. + +In the following example, we call the `ExitProcess` function from the Windows kernel library `kernel32.dll`. This module is loaded in every Windows process, so you can use its functions without having to inject a DLL. + +```csharp +var result = processMemory.RunThread("kernel32.dll", "ExitProcess"); +result.ThrowOnFailure(); // Throws an exception if the function could not be executed + +// Wait for the function to complete, with a timeout of 10 seconds +result.Value.WaitForCompletion(TimeSpan.FromSeconds(10)); + +// Dispose the result when done to ensure resources are released +result.Dispose(); +``` + +## Function parameters + +You can also pass parameters to the function you are calling. This is an advanced feature that requires you to understand how the target function expects its parameters to be passed. In MindControl, parameters are passed through a single pointer. You can arrange your parameters in a structure, store them in memory (e.g. using `Store`), and then pass the pointer to that memory as the parameter. + +```csharp +struct MyFunctionParams { public int Param1; public float Param2;} +var myParams = new MyFunctionParams { Param1 = 42, Param2 = 3.14f }; +var paramsPointer = processMemory.Store(myParams).Value.Address; +var result = processMemory.RunThread("myprocess.exe+019BAEA1", paramsPointer); +result.Value.WaitForCompletion(TimeSpan.FromSeconds(10)); +result.Dispose(); +``` + +However, this will only work for a specific argument passing convention. What this does is store the pointer in the RCX register (or EBX for 32-bit processes) before calling the function. + +If the function is not designed to accept parameters in this way, you will need to use a different approach, such as writing a wrapper function that prepares the parameters and calls the target function (also called a trampoline). + +Here is an example where we call the `GetCurrentDirectoryW` function from `kernel32.dll`, which expects two parameters: a buffer size and a pointer to a buffer where the current directory will be stored. + +> [!NOTE] +> The following example is complex and requires some understanding of assembly code and registers. For most use cases, you won't need to perform trampoline calls. Don't worry about this unless you need it. + +```csharp +// Define the structure that holds the parameters for GetCurrentDirectoryW +// The attribute prevents the compiler from adding padding bytes, ensuring the structure is packed tightly in memory. +[StructLayout(LayoutKind.Sequential, Pack = 1)] +struct GetCurrentDirectoryWArgs { public uint BufferSize; public ulong BufferAddress; } + +// Reserve memory for the buffer where the current directory will be stored +var bufferReservation = processMemory.Reserve(2048, false).Value; + +// Create an instance of the structure and store it in memory +var args = new GetCurrentDirectoryWArgs { BufferSize = 2048, BufferAddress = bufferReservation.Address }; +var argsReservation = processMemory.Store(args).Value; + +// Retrieve the address of the GetCurrentDirectoryW function +var kernel32Module = processMemory.GetModule("kernel32.dll"); +var functionAddress = kernel32Module!.ReadExportTable().Value["GetCurrentDirectoryW"]; + +// Using Iced.Intel, prepare the trampoline code to call GetCurrentDirectoryW +// Make sure to use "using static Iced.Intel.AssemblerRegisters;" to use the registers in a readable way +var assembler = new Assembler(64); +// In x64, the function uses the fastcall calling convention, i.e. RCX and RDX are used for the two arguments. +// When the thread is created, the thread parameter is in RCX. In this case, our parameter is going to be the +// address of a GetCurrentDirectoryWArgs struct holding the parameters we want. +assembler.mov(rax, rcx); // Move the address of the GetCurrentDirectoryWArgs struct to RAX, to free up RCX +assembler.mov(ecx, __dword_ptr[rax]); // Move the buffer size (first argument) to ECX/RCX +assembler.mov(rdx, __[rax+4]); // Move the buffer address (second argument) to RDX +assembler.call(functionAddress.ToUInt64()); // Call GetCurrentDirectoryW +assembler.ret(); + +// Store the trampoline code in the target process memory +// The nearAddress parameter is used to favor allocations close to the kernel32.dll module, for more efficient jumps +// This code requires the MindControl.Code package +var codeReservation = processMemory.StoreCode(assembler, nearAddress: kernel32Module.GetRange().Start).Value; + +// Now we can run the trampoline code in a new thread, passing the address of the GetCurrentDirectoryWArgs struct +var threadResult = processMemory.RunThread(codeReservation.Address, argsReservation.Address); +threadResult.Value.WaitForCompletion(TimeSpan.FromSeconds(10)).ThrowOnFailure(); + +// Read the resulting string from the allocated buffer +var resultingString = processMemory.ReadRawString(bufferReservation.Address, Encoding.Unicode, 512).Value; +Console.WriteLine($"Current Directory: {resultingString}"); // This should print the directory of the target process + +// Dispose everything to ensure resources are released +bufferReservation.Dispose(); +argsReservation.Dispose(); +codeReservation.Dispose(); +threadResult.Dispose(); +``` diff --git a/docs/guide/mcfeatures/monitoring.md b/docs/guide/mcfeatures/monitoring.md new file mode 100644 index 0000000..619757f --- /dev/null +++ b/docs/guide/mcfeatures/monitoring.md @@ -0,0 +1,27 @@ +# Monitoring value changes in memory + +This section covers how to monitor changes to a variable in memory. This can be useful if you need your application to react to changes in your target process, without having to set up timers or other polling mechanisms yourself. + +You can monitor changes to a variable in memory through anchors. An anchor is a persistent reference to a specific variable in memory, of a specific type, and it allows you to perform various operations on that specific target. + +```csharp +var anchor = processMemory.GetAnchor(); +var watcher = anchor.Watch(TimeSpan.FromMilliseconds(100)); // Read every 100 milliseconds +watcher.ValueChanged += (_, args) => +{ + Console.WriteLine($"Value changed from {args.PreviousValue} to {args.NewValue}."); +}; +watcher.ValueLost += (_, args) => +{ + Console.WriteLine($"Value lost. Last known value is {args.LastKnownValue}."); +}; +watcher.ValueReacquired += (_, args) => +{ + Console.WriteLine($"Value no longer lost. The new value is {args.NewValue}."); +}; + +// ... + +// To stop watching the value, dispose the watcher +watcher.Dispose(); +``` diff --git a/docs/guide/mcfeatures/pointer-paths.md b/docs/guide/mcfeatures/pointer-paths.md new file mode 100644 index 0000000..232e9d2 --- /dev/null +++ b/docs/guide/mcfeatures/pointer-paths.md @@ -0,0 +1,91 @@ +# Understanding pointer paths + +Most methods in the `ProcessMemory` class that accept an address also accept a pointer path. A pointer path is a string that describes a sequence of memory addresses and offsets, allowing you to navigate through complex data structures in the target process's memory. More importantly, they allow you to get to the value you want reliably, no matter how the target process shuffles its memory around. + +They are usually represented by a string called a pointer path expression. Here is an example of a pointer path expression: + +`UnityPlayer.dll+0168EEA0,8,100,28,20,80` + +In this example, the pointer path expression consists of: +- `UnityPlayer.dll+0168EEA0`: This is the base address of the module (DLL) in the target process's memory, with an offset of `0168EEA0` bytes. +- `8,100,28,20,80`: These are offsets that will be applied to the base address to navigate through the memory structure. Each number represents an offset in bytes from the previous address. + +This expression is the equivalent of the following pointer in Cheat Engine: + +![Pointer in Cheat Engine](../../images/ce-pointer-example.png)
+*To convert a pointer in Cheat Engine to a pointer path expression, read it from bottom to top, separating every field with a comma.* + +Pointer paths don't have to be based on modules, they can also be based on pointers. For example, if you have a base pointer address of `0x4A018C30A`, you can create a pointer path like this: + +`0x4A018C30A,8,100,28,20,80` + +You can also use `+` and `-` operators within pointer paths, e.g.: + +`UnityPlayer.dll+0168EEA0,100+8C,28-4,20,80` + +## Building pointer paths + +Pointer paths can be built in a number of ways: + +### Using the expression constructor + +```csharp +PointerPath myPath = new PointerPath("UnityPlayer.dll+0168EEA0,8,100,28,20,80"); +``` + +### Using PointerPath.TryParse + +Use this method when your pointer path is coming from an external source, such as user input or a configuration file. It will return `null` if the parsing fails, allowing you to handle errors gracefully. + +```csharp +var path = PointerPath.TryParse("UnityPlayer.dll+0168EEA0,8,100,28,20,80"); +if (path != null) +{ + // Successfully parsed the pointer path +} +else +{ + // Failed to parse the pointer path +} +``` + +### Using implicit string conversion + +This is a convenient way to create a `PointerPath` from a string without using the constructor directly. The string will be parsed automatically. + +```csharp +PointerPath myPath = "UnityPlayer.dll+0168EEA0,8,100,28,20,80"; +``` + +This can also be used with any method that accepts a `PointerPath` as an argument: + +```csharp +var someValue = processMemory.Read("UnityPlayer.dll+0168EEA0,8,100,28,20,80").ValueOrDefault(); +``` + +> [!NOTE] +> When using pointer string expressions, it's more performant to store and reuse your `PointerPath` instances instead of creating new ones every time you need to read or write memory. This avoids the overhead of parsing the string each time. + +### Using the module constructor + +This is more performant than the expression constructor, as it does not require parsing the string. + +```csharp +PointerPath myPath = new PointerPath("UnityPlayer.dll", 0x168EEA0, 0x8, 0x100, 0x28, 0x20, 0x80); +``` + +### Using the pointer constructor + +This is a variant of the module constructor that allows you to specify the base address as a pointer when your path is not based on a module. + +```csharp +PointerPath myPath = new PointerPath(0x4A018C30A, 0x8, 0x100, 0x28, 0x20, 0x80); +``` + +## Evaluating a pointer path + +When using a pointer path for memory manipulations, behind the scenes, MindControl will start by evaluating the pointer path to get the target address out of it. In some cases, you may want to do just that and nothing else. For this, you can use the `EvaluateMemoryAddress` method. + +```csharp +UIntPtr address = processMemory.EvaluateMemoryAddress("UnityPlayer.dll+0168EEA0,8,100,28,20,80").ValueOrDefault(); +``` diff --git a/docs/guide/mcfeatures/reading.md b/docs/guide/mcfeatures/reading.md new file mode 100644 index 0000000..2008f3e --- /dev/null +++ b/docs/guide/mcfeatures/reading.md @@ -0,0 +1,94 @@ +# Reading memory + +This section will explain how to read data from the target process using MindControl. + +> [!NOTE] +> Reading string values is a more complex topic and is covered in the [Manipulating string](./strings.md) section of this guide. + +## Reading numeric values + +To read numeric values from the target process, you can use the `Read` method of the `ProcessMemory` class. This method takes either an address or a pointer path, and returns a result containing either the read value in the asked type, or a failure in case the read operation failed. + +Most numeric types are supported, including `int`, `float`, `double`, `long`, `bool`, and others. The read operation will attempt to read the value from the specified address or pointer path in the target process's memory. + +Here are some examples of how to read numeric values from the target process: + +### Reading an integer value from a pointer path + +```csharp +var processMemory = MindControl.ProcessMemory.OpenProcessByName("MyTargetProcess").Value; +// Read an integer value from the specified address. Default to 0 if the read fails. +int health = processMemory.Read("GameAssembly.dll+12345678").ValueOr(0); +``` + +### Reading a float value from an address + +```csharp +var processMemory = MindControl.ProcessMemory.OpenProcessByName("MyTargetProcess").Value; +// Read a float value from the specified address. Default to 0.0f if the read fails. +float speed = processMemory.Read(0x1A2B3C4).ValueOr(0.0f); +``` + +## Reading arbitrary structures + +When you need to read multiple values in the same structure, you can define a struct that represents the data structure you want to read, and then use the same `Read` method to read the entire structure at once. This is more performant than reading each field individually, especially when using pointer paths. + +In most cases, you won't need all the fields of the structure, so you can define only the fields you are interested in. To handle these cases, you can use `[FieldOffset]` attributes to specify the offset of each field in the structure. This allows you to define a structure that only contains the fields you need, while still being able to read the entire structure in a single read operation. + +Here are some examples: + +### Reading a custom unmarked structure + +```csharp +// Define a structure that represents the data you want to read +// Fields must be in the same order as they are in memory +struct PlayerStats +{ + public int Health; + public float Speed; + public long Score; +} + +var processMemory = MindControl.ProcessMemory.OpenProcessByName("MyTargetProcess").Value; +PlayerStats playerStats = processMemory.Read("GameAssembly.dll+12345678").ValueOrDefault(); +``` + +### Reading a custom structure with field offsets + +```csharp +using System.Runtime.InteropServices; +// Define a structure with explicit field offsets +// This allows you to read only the fields you are interested in, even if they are not contiguous in memory. +[StructLayout(LayoutKind.Explicit)] // This is required for field offsets to be respected +struct PlayerStats +{ + [FieldOffset(0x00)] public int Health; + [FieldOffset(0x0A)] public float Speed; + [FieldOffset(0xF0)] public long Score; +} + +var processMemory = MindControl.ProcessMemory.OpenProcessByName("MyTargetProcess").Value; +PlayerStats playerStats = processMemory.Read("GameAssembly.dll+12345678").ValueOrDefault(); +// Remarks: even though only 3 fields are defined, the structure is 0xF8 bytes long, because it covers the whole memory +// area from 0x00 to the highest field offset plus its length. Don't use this approach if your fields are too far apart. +``` + +## Reading raw bytes + +Sometimes, you may want to read raw bytes, without interpreting them as a specific type. You can use the `ReadBytes` method of the `ProcessMemory` class to read a specified number of bytes from a given address or pointer path. + +```csharp +// Read 16 bytes from the specified address in the target process +byte[] rawData = processMemory.ReadBytes("GameAssembly.dll+12345678", 16).ValueOr([]); +``` + +There is also a `ReadBytesPartial` method variant that takes a `byte[]` array as a parameter, and populates it with the read data. Contrary to `ReadBytes`, this method will not fail if only some bytes could be read, and will return the number of bytes that were actually read. + +```csharp +// Read up to 2048 bytes into a buffer, and get the number of bytes actually read +byte[] buffer = new byte[2048]; +int bytesRead = processMemory.ReadBytesPartial("GameAssembly.dll+12345678", buffer, 2048).ValueOr(0); +``` + +> [!NOTE] +> `ReadBytesPartial` is only useful in particular cases, when reading large batches of an unreliable memory area. Most of the time, `ReadBytes` is simpler to use and preferable. diff --git a/docs/guide/mcfeatures/results.md b/docs/guide/mcfeatures/results.md new file mode 100644 index 0000000..a2ba21e --- /dev/null +++ b/docs/guide/mcfeatures/results.md @@ -0,0 +1,79 @@ +# Understanding Results + +Most methods in `MindControl` return a `Result` or `Result` object, which encapsulates the success or failure of the operation, along with any relevant data or error messages. This allows you to handle errors gracefully and provides a consistent way to check the outcome of operations. + +For example, when you read a value from a process, you can check if the operation was successful and retrieve the value if it was: + +```csharp +Result readResult = processMemory.Read("GameAssembly.dll+12345678"); +``` + +Instead of directly returning an `int`, and throwing an exception or returning `0` on failure, we get a `Result`. You can then check if the read operation was successful using the `IsSuccess` property: + +```csharp +if (readResult.IsSuccess) +{ + int value = readResult.Value; + // Use the value as needed +} +else +{ + // Handle the error + Console.WriteLine($"Error reading value: {readResult.Failure.Message}"); +} +``` + +This pattern is used throughout the `MindControl` library, allowing you to handle errors in a consistent way without relying on exceptions for control flow. + +Even though relying on exceptions is generally discouraged, because they are slower and tend to make the code harder to read, you can use the `ThrowOnFailure()` method to throw an exception if the operation failed, which can be useful in scenarios where you want to enforce error handling: + +```csharp +readResult.ThrowOnFailure(); // Throws an exception if the read operation failed +int value = readResult.Value; // After the exception check, you can safely use the value +``` + +> [!NOTE] +> Accessing the `Value` property of an unsuccessful `Result` will also throw an exception. + +If you prefer to discard errors and just use default values, you can use the `ValueOrDefault()` method, which returns the value if the operation was successful, or a default value (like `0` for numeric types) if it failed: + +```csharp +int value = processMemory.Read("GameAssembly.dll+12345678").ValueOrDefault(); +// value will be 0 if the read operation failed +``` + +Alternatively, you can use `ValueOr()` to provide a custom default value: + +```csharp +int value = processMemory.Read("GameAssembly.dll+12345678").ValueOr(42); +// value will be 42 if the read operation failed +``` + +## The Failure object + +When an operation fails, the `Result` object contains a `Failure` property that provides detailed information about the error. The `Failure` base class itself contains only a `Message` property, that describes the error, but methods usually return a more specific type of `Failure` that provides additional context. + +```csharp +// Ask the user for a pointer path +string pointerPath = Console.ReadLine(); + +// Use the pointer path to read a value +var readResult = processMemory.Read(pointerPath); +if (!readResult.IsSuccess) +{ + // Handle the error in a different way based on the specific failure type + // In this example, we just print a different message for each failure type, but the idea is that you can perform + // different actions based on the type of failure if you need to. + readResult.Failure switch + { + BaseModuleNotFoundFailure f => Console.WriteLine($"The module you entered ({f.ModuleName}) is invalid!"), + DetachedProcessFailure _ => Console.WriteLine($"The process has exited."), + IncompatiblePointerPathBitnessFailure _ => Console.WriteLine("The pointer path you entered is not compatible with the 32-bit target process!"), + _ => Console.WriteLine($"An unexpected error occurred: {readResult.Failure}") + }; + return; +} + +int value = readResult.Value; +// (Use the value as needed) +``` diff --git a/docs/guide/mcfeatures/searching.md b/docs/guide/mcfeatures/searching.md new file mode 100644 index 0000000..e185ac0 --- /dev/null +++ b/docs/guide/mcfeatures/searching.md @@ -0,0 +1,52 @@ +# Searching for byte sequences or patterns + +This section explains how to search through the memory of the target process for specific byte sequences or byte patterns. This type of search is sometimes called "AoB (Array of Bytes) scanning", and it's useful to find specific data arrangements in memory, especially in cases where pointer paths fail or are harder to pull off. + +Let's jump right into an example to make things clearer. + +```csharp +IEnumerable results = processMemory.SearchBytes("90 A8 00 00 ?? 42 A8"); +``` + +In this example, we are searching for a specific byte pattern in the target process's memory. The pattern consists of some set hexadecimal byte values, and the `??` is a wildcard that matches any byte value at that position. The search will return all addresses where this pattern is found. + +So this example would match, for instance, `90 A8 00 00 01 42 A8` or `90 A8 00 00 FF 42 A8`, but not `90 A8 00 01 42 A8`. + +Now, this example searches the entire memory of the target process, which is usually very slow, especially if your target process uses up a lot of memory. For modern 3D games, this can easily take over a minute. Let's dive into how to make this search more efficient. + +> [!NOTE] +> Do not start your patterns with a wildcard (`??`). Even though this is supported, it will slow down the search significantly, and has no practical purpose. You can remove leading wildcards from your patterns to achieve the same result without the performance hit. + +## Restricting the search range + +The second parameter of the `SearchBytes` method allows you to specify a range of memory addresses to search in. This can significantly speed up the search process, especially if you know where the data you're looking for is likely to be located. + +```csharp +MemoryRange range = new MemoryRange(0x10000000, 0x20000000); +IEnumerable results = processMemory.SearchBytes("90 A8 00 00 ?? 42 A8", range); +``` + +The typical way to use this parameter is to get a specific module in the target process, and then search only within that module's memory range. Here's how: + +```csharp +RemoteModule module = processMemory.GetModule("GameAssembly.dll") ?? throw new Exception("Module not found"); +IEnumerable results = processMemory.SearchBytes("90 A8 00 00 ?? 42 A8", module.GetRange()); +``` + +## Specifying settings to filter out invalid results + +Another way to both speed up the search *and* filter out unwanted results is to use the third `FindBytesSettings` parameter. This allows you to specify additional criteria for the search, to ignore certain ranges of memory depending on their properties, or to specify a maximum number of results. + +```csharp +RemoteModule module = processMemory.GetModule("GameAssembly.dll") ?? throw new Exception("Module not found"); +var settings = new FindBytesSettings +{ + SearchReadable = true, // Only search in readable memory + SearchWritable = null, // Don't care if the memory is writable or not + SearchExecutable = false, // Ignore executable memory, because we are looking for data, not code + MaxResultCount = 10 // Limit the number of results to 10 +}; +IEnumerable results = processMemory.SearchBytes("90 A8 00 00 ?? 42 A8", range, settings); +``` + +Using these settings can greatly improve the performance of your searches, but keep in mind that they are still extremely slow compared to pointer paths or direct memory reads. Use them only when you believe this is the best way to find the data you are looking for. diff --git a/docs/guide/mcfeatures/streams.md b/docs/guide/mcfeatures/streams.md new file mode 100644 index 0000000..29dfc23 --- /dev/null +++ b/docs/guide/mcfeatures/streams.md @@ -0,0 +1,26 @@ +# Using memory streams + +For various use cases, you may need to read or write memory using streams. For these situations, MindControl provides a `ProcessMemoryStream` class, instantiated through a `ProcessMemory` instance. + +This can be useful to read or write large amounts of data in a more efficient way, or more generally to work with memory as if it were a file. + +```csharp +// Get a stream that starts at a specific address in the target process +ProcessMemoryStream stream = processMemory.GetMemoryStream(0x123456789); + +// Read from the stream +byte[] buffer = new byte[1024]; +int bytesRead = stream.Read(buffer, 0, buffer.Length); + +// Write to the stream +stream.Write(buffer, 0, bytesRead); + +// Seek to a specific position in the stream (here, go 8 bytes after the initial address 0x123456789) +stream.Seek(8, SeekOrigin.Begin); + +// Dispose the stream when done +stream.Dispose(); +``` + +> [!NOTE] +> Internally, the stream uses `ReadPartial` so that it will still read as much as possible upon reaching an unreadable section of memory. When failing to read a single byte, the `Read` method will return 0. \ No newline at end of file diff --git a/docs/guide/mcfeatures/strings.md b/docs/guide/mcfeatures/strings.md new file mode 100644 index 0000000..ccafde0 --- /dev/null +++ b/docs/guide/mcfeatures/strings.md @@ -0,0 +1,180 @@ +# Manipulating strings + +Reading and writing strings in a process's memory is more complex than numeric values and requires understanding how strings are represented in memory. In this section, we will cover how strings are stored in memory, and how to manipulate them with `MindControl`. + +## Understanding string representation in memory + +Intuitively, strings are sequences of characters, but in memory, they are represented as a sequence of bytes. In some way or another, these bytes represent the characters in the string. But because their length is variable, **they are almost always referenced by a pointer**. For example, a `Player` structure could have a `Name` field that holds a pointer to the start of the string in memory. + +Now, once you locate a string, the key is then to understand how to translate its bytes into characters and vice versa. + +### Encodings + +The thing is, there are many ways to represent strings in memory, and it mostly comes down to what is called the "encoding". The encoding defines how characters are mapped to bytes. Some common encodings are: +- **ASCII**: Uses 1 byte per character, supports only the first 128 characters (English letters, digits, and some symbols). +- **UTF-8**: Uses 1 to 4 bytes per character, supports all Unicode characters, like symbols, chinese characters, emojis, etc. It is the most common encoding used in modern applications. +- **UTF-16**: Uses 2 bytes per character, also supports all Unicode characters, and is commonly used in Windows applications. + +You may also see other encodings like UTF-32, ISO-8859-1, or others. It all depends on the application and how it was developed. And in a single application, you may find different encodings used for different use cases, because some encodings are more efficient for certain types of data. For example, you may decide to use UTF-16 for a player name, to handle any language, but still use ASCII for internal item identifiers, because you don't need more than the English alphabet, and it uses up less memory. + +Let's take a look at some examples of how the same string is represented in memory with different encodings. + +#### Example: "Hello, World!" in different encodings + +| Encoding | Bytes in Hexadecimal | +| --- | --- | +| ASCII | 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 | +| UTF-8 | 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 | +| UTF-16 | 48 00 65 00 6C 00 6C 00 6F 00 2C 00 20 00 57 00 6F 00 72 00 6C 00 64 00 21 00 | + +#### Example 2: "こんにちは" (Hello in Japanese) in different encodings + +| Encoding | Bytes in Hexadecimal | +| --- |----------------------------------------------| +| ASCII | (This string cannot be represented in ASCII) | +| UTF-8 | E3 81 93 E3 82 93 E3 81 AB E3 81 A1 E3 81 AF | +| UTF-16 | 53 30 93 30 6B 30 61 30 6F 30 | + +We can see several interesting things here: +- The ASCII and UTF-8 representations of "Hello, World!" are the same, because all characters in this string are part of the ASCII character set, and UTF-8 is designed to be backward compatible with ASCII. +- The Japanese string cannot be represented in ASCII, because it contains characters that are not part of the ASCII character set. However, it can be represented in both UTF-8 and UTF-16. +- UTF-16 always uses 2 bytes per character, which is kind of wasteful for ASCII-compatible strings like "Hello, World!" (you can see every second bit is a zero), but it ends up using way less space for the Japanese string. This is because it _always_ uses 2 bytes per character, while UTF-8 has variable-length characters, meaning it has to dedicate extra bits to indicate one way or another how many bytes are used for each character in the string. + +So, after locating a string in memory, we need to find out what encoding is used to represent it. + +### Knowing where the string stops + +When reading a string from memory, we also need to know where it ends. Because bytes in memory are not delimited and keep going on pretty much forever, it's important to know when to stop reading. And to do that, there are 2 main ways that programming languages and libraries use: +- **Null-terminators**: This is a special byte (`00`) or group of bytes (`00 00`) appended after the final character that indicates the end of the string. When reading a string, you keep reading bytes until you encounter a null terminator. This is common in C-style strings and is used in many programming languages. +- **Length prefix**: This is a byte or group of bytes before the first character of the string, that indicates how many bytes or characters follow it. When reading a string, you read the length first, and then read that many bytes. This is common in stacks like .net and Java. + +Sometimes, both techniques are used together, for compatibility. + +#### Example 1: Reading a string with a null terminator + +Let's say we have a string "Hello, World!" stored in memory as UTF-8, and we know it is null-terminated. The bytes in memory would look like this: + +``` +48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 00 +``` + +When reading this string, we would know to stop reading as soon as we encounter the null terminator `00`. The resulting string would be "Hello, World!". + +#### Example 2: Reading a string with a length prefix + +Now, let's say we have the same string "Hello, World!" stored in memory as UTF-8, but this time we have identified that it has a length prefix of 2 bytes. The bytes in memory would look like this: + +``` +0D 00 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 +``` + +When reading this string, knowing that it has a length prefix of 2 bytes, we would start by reading the length prefix `0D 00`. This is the number 13, meaning that the string is 13 bytes long. Then we would know to read only the next 13 bytes, resulting in the string "Hello, World!". + +### Type handles + +In some cases, especially in managed languages and frameworks like .net, strings, like any other object instance, start with a type handle. This is a pointer to a metadata structure that describes the type of the object (in this case, a string). This type handle is used for various purposes by the runtime. + +We don't care about this handle, but, when reading a string, we need to know if it exists and how long it is, so we can skip it. And when writing, we actually need to know the full handle, so we can write it properly, the way it would have been written by the runtime. + +#### An example of a typical .net string + +The .net standard for strings has all the things we've discussed so far, which makes it ideal for a final example. So let's take a look at a "Hello, World!" string in .net: + +``` +C0 12 34 56 78 9A BC DE 0D 00 00 00 48 00 65 00 6C 00 6C 00 6F 00 2C 00 20 00 57 00 6F 00 72 00 6C 00 64 00 21 00 00 00 +``` + +This string is represented in memory as follows: +- `C0 12 34 56 78 9A BC DE`: This is the .net type handle, which is a pointer to the metadata structure that describes the string type. This is an example, the actual handle will be different in every application, and even in every run of the same application. +- `0D 00 00 00`: This is the length prefix. In .net, length prefixes are 4 bytes long, and indicate the number of characters. This one reads as 13 (0x0D) characters long. +- `48 00 65 00 6C 00 6C 00 6F 00 2C 00 20 00 57 00 6F 00 72 00 6C 00 64 00 21 00`: These are the UTF-16 encoded characters of the string "Hello, World!", with each character taking up two bytes. +- `00 00`: This is the null terminator, which indicates the end of the string. Because we are using UTF-16, the null terminator is represented as two bytes and not just one. We technically don't need it, because we have the length prefix, but .net still has this terminator for compatibility with other systems that expect it. + +## Reading strings with MindControl + +There are several options to read strings in MindControl, depending on your needs and preferences. + +### Using ReadStringPointer + +The `ReadStringPointer` method of the `ProcessMemory` class is the most versatile way to read strings from memory. It takes an address or a pointer path to the pointer to the string you want to read, along with a `StringSettings` object that defines how the string is represented in memory. This method will read the pointer, then read the string from the address it points to, and return the string value. + +Let's take an example to read the string from the previous example (a standard .net string): + +```csharp +var stringSettings = new StringSettings( + encoding: System.Text.Encoding.UTF8, + isNullTerminated: true, + lengthPrefix: new LengthPrefix(4, StringLengthUnit.Characters), // 4 byte length, counting characters (not bytes) + typePrefix: new byte[] { 0xC0, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE }); + +string? result = processMemory.ReadStringPointer("GameAssembly.dll+12345678", stringSettings).ValueOrDefault(); +``` + +In this example, we specify the encoding as UTF-8, the string is null-terminated, and it has a 4-byte length prefix that counts characters. We also provide the type handle as a byte array. The method will read the pointer at the specified address, then read the string from the address it points to, and return the string value. + +> [!NOTE] +> As stated before, this method takes the address of a pointer to the string, **not** the address of the start of the string itself. + +### Using ReadRawString + +An alternative to `ReadStringPointer` is the `ReadRawString` method, which reads a string directly from the address or pointer path to its first character byte, without needing to provide a pointer to the string. This method is useful when you don't have a pointer to the string, or if you prefer to read the string directly from its starting address. + +Instead of a `StringSettings` object, this method takes an `Encoding` parameter to specify how the string is encoded, a max length that indicates when to stop, and a boolean to indicate if the string is null-terminated. + +A matching example for the previous string would look like this: + +```csharp +// Note the 'C' offset at the end of the pointer path to skip the type handle (8 bytes) and the length prefix (4 bytes) +string? result = processMemory.ReadRawString("GameAssembly.dll+12345678,C", + Encoding.UTF8, + maxLength: 100, + isNullTerminated: true).ValueOrDefault(); +``` + +> [!NOTE] +> ReadRawString stops reading when it reaches the specified maximum length, or when it encounters a null terminator, whichever comes first. If you specify no null terminator, it will always read up to the maximum length. + +### Determining the string settings automatically + +If you don't know the string settings, you can use observation and trial and error to determine them. However, `MindControl` provides a method that helps you pinpoint the string settings automatically. The `FindStringSettings` method takes an address or pointer path to a pointer to a string, and the expected string value, and returns a `StringSettings` object that matches the string representation in memory. + +It may not be ideal for all cases, especially because it requires you to know a string value that you expect to find in memory, but it can be useful especially when dealing with strings with a type handle that changes every time the application is run. + +```csharp +// Determine the string settings automatically thanks to a known "Hello, World!" string somewhere in memory +var expectedString = "Hello, World!"; +StringSettings stringSettings = processMemory.FindStringSettings("GameAssembly.dll+12345678", expectedString) + .ValueOrDefault(); + +// Now you can use the string settings to read another string that you don't know +string? anotherString = processMemory.ReadStringPointer("GameAssembly.dll+ABCDEF012", stringSettings).ValueOrDefault(); +``` + +## Writing strings with MindControl + +By design, MindControl does not have a method that directly writes strings to memory. This is because it would make it too easy for users to make mistakes and either corrupt memory, or bloat memory with hundreds of thousands of strings that are never erased. + +This is because, unless you are certain that your new string is smaller than the original string you want to replace, you cannot simply overwrite the bytes of the original string with the bytes of your new string. If the new string is longer, you would end up writing past the end of the original string, which could corrupt memory and lead to crashes or unexpected behavior. + +Instead, you can write strings to memory by chaining multiple operations: +- (Optional) Use `FindStringSettings` on a known string to determine the string settings for the string you want to write. +- Use `StoreString` to store the value of the string somewhere in an allocated space in memory. +- Use `Write` to overwrite the pointer to the string with the address of your newly stored string. + +Here is a concrete example of exactly that: + +```csharp +// Determine the string settings automatically thanks to a known "Hello, World!" string +var expectedString = "Hello, World!"; +StringSettings stringSettings = processMemory.FindStringSettings("GameAssembly.dll+12345678", expectedString) + .ValueOrDefault(); + +// Store the new string in memory using the determined string settings +MemoryReservation reservation = processMemory.StoreString("New String Value", stringSettings).ValueOrDefault(); + +// Write the pointer to the new string in the target process memory +processMemory.Write("GameAssembly.dll+12345678", reservation.Address).ThrowOnFailure(); +``` + +> [!NOTE] +> Make sure to dispose of the `MemoryReservation` object when you no longer need it, to avoid memory leaks in the target process. If you constantly write new strings in this way without disposing of the old reservations, you will quickly end up consuming gigabytes of memory in the target process, which will lead to performance issues and crashes. diff --git a/docs/guide/mcfeatures/writing.md b/docs/guide/mcfeatures/writing.md new file mode 100644 index 0000000..fc5d2cc --- /dev/null +++ b/docs/guide/mcfeatures/writing.md @@ -0,0 +1,85 @@ +# Writing to memory + +This section will guide you through writing to memory in a target process using `MindControl`. + +> [!NOTE] +> Writing string values is a more complex topic and is covered in the [Manipulating string](./strings.md) section of this guide. + +## Writing numeric values + +To write numeric values to the target process, you can use the `Write` method of the `ProcessMemory` class. This method takes either an address or a pointer path, and the value to write, and returns a result indicating whether the write operation was successful. + +Here are some examples of how to write numeric values to the target process: + +### Writing an integer value to a pointer path + +```csharp +var processMemory = MindControl.ProcessMemory.OpenProcessByName("MyTargetProcess").Value; +bool success = processMemory.Write("GameAssembly.dll+12345678", 100).IsSuccess; +``` + +### Writing a float value to an address + +```csharp +var processMemory = MindControl.ProcessMemory.OpenProcessByName("MyTargetProcess").Value; +bool success = processMemory.Write(0x1A2B3C4, 3.14f).IsSuccess; +``` + +## Writing arbitrary structures + +Just like when reading, you can define a struct that represents the data structure you want to write, and then use the `Write` method to write the entire structure at once. This is more efficient than writing each field individually. + +Here are some examples: + +### Writing a custom unmarked structure + +```csharp +// Define a structure that represents the data you want to write +// Fields must be in the same order as they are in memory +struct PlayerStats +{ + public int Health; + public float Speed; + public long Score; +} +var processMemory = MindControl.ProcessMemory.OpenProcessByName("MyTargetProcess").Value; +bool success = processMemory.Write("GameAssembly.dll+12345678", new PlayerStats { Health = 100, Speed = 5.0f, Score = 1000 }).IsSuccess; +``` + +### Writing a custom structure with field offsets + +```csharp +using System.Runtime.InteropServices; +// See the previous section for more details on how to define structures with field offsets. +[StructLayout(LayoutKind.Explicit)] +struct PlayerStats +{ + [FieldOffset(0x00)] public int Health; + [FieldOffset(0x0A)] public float Speed; + [FieldOffset(0xF0)] public long Score; +} +var processMemory = MindControl.ProcessMemory.OpenProcessByName("MyTargetProcess").Value; +bool success = processMemory.Write("GameAssembly.dll+12345678", new PlayerStats { Health = 100, Speed = 5.0f, Score = 1000 }).IsSuccess; +``` + +## Writing raw bytes + +If you need to write raw bytes to a specific address or pointer path, you can use the `WriteBytes` method. This method takes a byte array and writes it to the specified location in the target process. + +```csharp +byte[] dataToWrite = new byte[] { 0x90, 0x90, 0x90 }; +bool success = processMemory.WriteBytes("GameAssembly.dll+12345678", dataToWrite).IsSuccess; +``` + +## Memory protection strategies + +All writing methods in `MindControl` have an additional parameter that allows you to specify how to handle memory protection. There are three options: +- `MemoryProtectionStrategy.Ignore`: No protection removal. The write will fail if the memory is protected, but if you know it isn't, this is the most performant option. +- `MemoryProtectionStrategy.Remove`: Removes the memory protection before writing. Memory protection will not be restored after the write operation, which may cause issues in some cases, but may be more performant than restoring it. +- `MemoryProtectionStrategy.RemoveAndRestore`: Temporarily removes the memory protection to allow writing, then restores it. This is the safest, but least performant option. + +The default strategy is `MemoryProtectionStrategy.RemoveAndRestore`, for best compatibility. You can change this by passing a different strategy to the `Write` method. + +```csharp +bool success = processMemory.Write("GameAssembly.dll+12345678", 100, MemoryProtectionStrategy.Ignore).IsSuccess; +``` \ No newline at end of file diff --git a/docs/guide/project-setup/blazor-setup.md b/docs/guide/project-setup/blazor-setup.md new file mode 100644 index 0000000..6a3c64c --- /dev/null +++ b/docs/guide/project-setup/blazor-setup.md @@ -0,0 +1,171 @@ +# Setting up MindControl in a Blazor (web) application + +This section will guide you through setting up a new Blazor application project that uses the `MindControl` library to interact with the memory of a target process. We will use the Slime Rancher demo (see the [requirements page](../guide-requirements.md) for more info) as our target process, but feel free to use any other target. + +## Define the scope of your project + +Before we start, let's define what our project should do. In our example, we are going to set up a very simple page, with an editable field for the player's current coin count, with real-time synchronization. + +## Create a new Blazor project + +First, we need to create a new Blazor application project. There are two types of Blazor applications: Blazor Server and Blazor WebAssembly. Explaining the differences is beyond the scope of this guide, but Blazor server is perfect for our purposes, because it has a built-in mechanism to constantly stream updates from the server app to the browser. + +Open a command line interface and navigate to the directory where you want to create the project. Then, run the following command to create your project from scratch: + +```bash +dotnet new blazor -n MyMindControlWebProject --empty +cd MyMindControlWebProject +``` + +> [!NOTE] +> We are using the `dotnet` command-line tool here, but feel free to use your IDE of choice if you prefer. + +## Add the MindControl library to your project + +Next, we need to add the `MindControl` library to our project. Run the following command to add the library to your project: + +```bash +dotnet add package MindControl +``` + +This will reference the latest stable version of the `MindControl` library in your project using NuGet. + +## Develop your memory hacking features in a new class + +It's a good idea to separate your memory hacking features from the rest of your application. This way, you can keep your code organized and easy to maintain. Let's create a new class called `SlimeRancherDemo` in a new file called `SlimeRancherDemo.cs`. This class will be our entry point for interacting with the target process, and will define methods for every memory hack our program is able to do. + +```csharp +using MindControl; + +namespace MyMindControlWebProject; + +public class SlimeRancherDemo +{ + // We have determined the memory path to the coin count value in the target process using Cheat Engine. See the tutorials in this guide to learn how to do this. + private readonly PointerPath _coinCountPath = "UnityPlayer.dll+0168EEA0,8,100,28,20,80"; + + // Use the name of your target process here. + private readonly ProcessTracker _processTracker = new("SlimeRancher"); + + public int? GetCoinCount() + { + var process = _processTracker.GetProcessMemory(); + if (process == null) + return null; // The target process is not running + + // Try to read the coin count value from the target process + var coinCountResult = process.Read(_coinCountPath); + if (!coinCountResult.IsSuccess) + { + // The coin count value could not be read (maybe we are in the main menu) + // Check coinCountResult.Failure for more information + return null; + } + + return coinCountResult.Value; + } + + public bool SetCoinCount(int newCoinCount) + { + var process = _processTracker.GetProcessMemory(); + if (process == null) + return false; // The target process is not running + + // Try to write the new coin count value to the target process, and return true if successful + var writeResult = process.Write(_coinCountPath, newCoinCount); + return writeResult.IsSuccess; + } +} +``` + +> [!NOTE] +> In the next chapter of this guide, we will explain how to use the classes and methods provided by `MindControl`. For now, we are just focusing on getting a basic project set up. + +As you can see, in our case, we defined two methods: `GetCoinCount` and `SetCoinCount`. + +## Register the service in the Program.cs file + +Now that we have our memory hacking features defined, we need to register the `SlimeRancherDemo` service in the `Program.cs` file so that we can use it in our Blazor components. + +Open the `Program.cs` file and add the following code right before the `builder.Build()` line: + +```csharp +// Register the SlimeRancherDemoService as a singleton, so it can be used by multiple pages and components. +builder.Services.AddSingleton(); +``` + +## Implementing the page + +Now that we have our memory hacking features defined, let's work on the page itself. In Blazor, pages are defined in `.razor` files. The template already has a home page in `Components/Pages/Home.razor`, so let's edit that file to make our page. + +```html +@page "/" + +@* Specifying the render mode as InteractiveServer allows us to use server-side rendering with interactive components, +which is crucial in our case because the client (the browser) cannot directly access the Slime Rancher game state. *@ +@rendermode InteractiveServer + +@* We can access the SlimeRancherDemo service using injection because we have registered it in the Program.cs file *@ +@inject SlimeRancherDemo _slimeRancherDemo + +Home + +
+ + +
+ +@code { + public int _coinCount; + + private readonly System.Timers.Timer _stateUpdateTimer = new System.Timers.Timer(TimeSpan.FromMilliseconds(100)); + protected override void OnInitialized() + { + _stateUpdateTimer.Elapsed += OnStateUpdateTimerElapsed; + _stateUpdateTimer.Start(); + } + + /// Called when the timer ticks, every 100ms. + private void OnStateUpdateTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e) + { + _coinCount = _slimeRancherDemo.GetCoinCount() ?? 0; + + // Update the UI with StateHasChanged. We need to do that manually when new data comes from events or timers. + InvokeAsync(StateHasChanged); + } + + /// Called when the coin input value is modified manually. + private void OnCoinsModified(ChangeEventArgs e) + { + // Make sure the input value is a valid number + if (!int.TryParse(e.Value?.ToString(), out int coins)) + return; + + _slimeRancherDemo.SetCoinCount(coins); + } +} +``` + +This code defines a simple page with an input field for the coin count. The input field is bound to the `_coinCount` variable, which is updated every 100 milliseconds by the timer we set up in the `OnInitialized` method. + +When the user modifies the input field, the `OnCoinsModified` method is called (through the `oninput` event), which updates the coin count in the game. + +At this stage, the program should be complete and functional. You can run it using `dotnet run` from the project directory and try it out. + +> [!NOTE] +> You may need to manually access the page in your browser by navigating to the link output by the application after running it, which should look like `http://localhost:5273`. + +![Our example Blazor application](../../images/ex-blazor-app-simple.png) + +> Try spending or earning coins in the game, and see how the coin count updates in real-time in your application. You can also change the coin count value in the input field, and it will be reflected in the game. + +## Going further + +Now, to learn how to use classes and methods provided by the `MindControl` library, check out the [next chapter](../mcfeatures/attaching.md) of this guide. + +Alternatively, if you want to see a more advanced example of a Blazor application using the `MindControl` library, check out the [MindControl Blazor app sample](https://github.com/Doublevil/mind-control/tree/main/samples/MindControl.Samples.SrDemoBlazorApp) in the MindControl repository. Here is a breakdown of the features in the sample: +- Fully automated process tracking (process detection with no need for user input, supports the game closing and restarting). +- Editable sliders and input fields, synchronized in real-time with the game, for the player's health, stamina, and coin count values. +- A button to toggle infinite stamina. + +![An example of the MindControl Blazor app sample](../../images/ex-blazor-app-full.png) diff --git a/docs/guide/project-setup/console-setup.md b/docs/guide/project-setup/console-setup.md new file mode 100644 index 0000000..a5e275d --- /dev/null +++ b/docs/guide/project-setup/console-setup.md @@ -0,0 +1,130 @@ +# Setting up MindControl in a console application + +This section will guide you through setting up a new console application project that uses the `MindControl` library to interact with the memory of a target process. We will use the Slime Rancher demo (see the [requirements page](../guide-requirements.md) for more info) as our target process, but feel free to use any other target. + +## Define the scope of your project + +Before we start, let's define what our project should do. In our example, we are going to set up a very simple program that reads the player's current coin count in our target game, and then after pressing a key, we will increment the coin count by 1000. + +## Create a new console application project + +First, we need to create a new console application project. Open a command line interface and navigate to the directory where you want to create the project. Then, run the following command to create your project from scratch: + +```bash +dotnet new console -n MyMindControlProject +cd MyMindControlProject +``` + +> [!NOTE] +> We are using the `dotnet` command-line tool here, but feel free to use your IDE of choice if you prefer. + +## Add the MindControl library to your project + +Next, we need to add the `MindControl` library to our project. Run the following command to add the library to your project: + +```bash +dotnet add package MindControl +``` + +This will reference the latest stable version of the `MindControl` library in your project using NuGet. + +## Develop your memory hacking features in a new class + +It's a good idea to separate your memory hacking features from the rest of your application. This way, you can keep your code organized and easy to maintain. Let's create a new class called `SlimeRancherDemo` in a new file called `SlimeRancherDemo.cs`. This class will be our entry point for interacting with the target process, and will define methods for every memory hack our program is able to do. + +```csharp +using MindControl; + +namespace MyMindControlProject; + +public class SlimeRancherDemo +{ + // We have determined the memory path to the coin count value in the target process using Cheat Engine. See the tutorials in this guide to learn how to do this. + private readonly PointerPath _coinCountPath = "UnityPlayer.dll+0168EEA0,8,100,28,20,80"; + + // Use the name of your target process here. + private readonly ProcessTracker _processTracker = new("SlimeRancher"); + + public int? GetCoinCount() + { + var process = _processTracker.GetProcessMemory(); + if (process == null) + return null; // The target process is not running + + // Try to read the coin count value from the target process + var coinCountResult = process.Read(_coinCountPath); + if (!coinCountResult.IsSuccess) + { + // The coin count value could not be read (maybe we are in the main menu) + // Check coinCountResult.Failure for more information + return null; + } + + return coinCountResult.Value; + } + + public bool SetCoinCount(int newCoinCount) + { + var process = _processTracker.GetProcessMemory(); + if (process == null) + return false; // The target process is not running + + // Try to write the new coin count value to the target process, and return true if successful + var writeResult = process.Write(_coinCountPath, newCoinCount); + return writeResult.IsSuccess; + } +} +``` + +> [!NOTE] +> In the next chapter of this guide, we will explain how to use the classes and methods provided by `MindControl`. For now, we are just focusing on getting a basic project set up. + +As you can see, in our case, we defined two methods: `GetCoinCount` and `SetCoinCount`. + +## Implement the main program logic + +Now that we have our memory hacking features defined, let's implement the main program logic in the `Program.cs` file. This is where we will interact with the `SlimeRancherDemo` class we just created. + +```csharp +using MyMindControlProject; + +var slimeRancher = new SlimeRancherDemo(); +Console.WriteLine("Press any key to read the current coin count."); +Console.ReadKey(true); +int? coinCount = slimeRancher.GetCoinCount(); +if (coinCount == null) +{ + Console.WriteLine("Could not read the coin count. Make sure the game is running, and a new game is started."); + return; +} + +Console.WriteLine($"Current coin count: {coinCount}"); +Console.WriteLine("Press any key to add 1000 coins."); +Console.ReadKey(true); + +bool isWriteSuccessful = slimeRancher.SetCoinCount(coinCount.Value + 1000); +if (!isWriteSuccessful) +{ + Console.WriteLine("Could not write the new coin count. Make sure the game is running, and a new game is started."); + return; +} +Console.WriteLine("Coin count updated successfully."); +``` + +This code will read the current coin count from the target process, display it to the user, and then increment the coin count by 1000. Feel free to modify this code to fit your own project. + +As of this stage, the program should be complete and functional. You can run it using `dotnet run` from the project directory and try it out. + +![Our example console application](../../images/ex-console-app-simple.png) + +## Going further + +Now, to learn how to use classes and methods provided by the `MindControl` library, check out the [next chapter](../mcfeatures/attaching.md) of this guide. + +Alternatively, if you want to see a more advanced example of a console application using the `MindControl` library, check out the [MindControl console app sample](https://github.com/Doublevil/mind-control/tree/main/samples/MindControl.Samples.SrDemoConsoleApp) in the MindControl repository. Here is a breakdown of the features in the sample: +- Fully automatic process tracking (process detection with no need for user input, supports the game closing and restarting, etc). +- A live view of the player's current coin count, health, and stamina values, automatically updated multiple times per second. +- Simple user input to change the player's coin count, health, and stamina values. +- A command to toggle infinite stamina. + +![An example of the MindControl console app sample](../../images/ex-console-app-full.png) diff --git a/docs/guide/project-setup/creating-mc-project.md b/docs/guide/project-setup/creating-mc-project.md new file mode 100644 index 0000000..614a8d2 --- /dev/null +++ b/docs/guide/project-setup/creating-mc-project.md @@ -0,0 +1,43 @@ +# Creating a MindControl project + +In this chapter, we are going to implement a simple example program that uses the `MindControl` library to interact with the memory of our example target process, the Slime Rancher demo. + +## Requirements + +Before we start, make sure the .net SDK is installed on your system. You can download it from the [official Microsoft .net website](https://dotnet.microsoft.com/en-us/download/dotnet). Pick the latest LTS (Long-Term Support) version. Alternatively, you can use Visual Studio or your .net IDE of choice, which should come with the .net SDK. + +To make sure everyone can follow equally, this guide will not assume any particular IDE and use the `dotnet` command-line tool included in the SDK, instead of IDE-specific instructions. + +This guide assumes you have a basic understanding of C# programming. If you are new to C#, you can learn in the [official Microsoft C# portal](https://docs.microsoft.com/en-us/dotnet/csharp/). + +## What do you want to build? + +This guide will show an implementation for multiple different scenarios. It's not necessary to read through them all, just pick the one you are most interested in building. + +### Console application + +Console apps are the simplest to develop, but also the most limited choice in terms of interface. + +![An example console application running MindControl code](../../images/ex-console-app-full.png) + +**[Follow the Console application implementation guide](./console-setup.md)** + +### WPF (desktop) application + +WPF apps provide a rich desktop interface that can be customized to your liking. + +![An example WPF application running MindControl code](../../images/ex-wpf-app-full.png) + +**[Follow the WPF application implementation guide](./wpf-setup.md)** + +### Blazor (web) application + +Blazor server apps are web applications. They can be accessed through a web browser, and provide a rich interface that can also be reached from other devices, as long as the server app runs on the same machine as the target process. + +![An example Blazor application running MindControl code](../../images/ex-blazor-app-full.png) + +**[Follow the Blazor application implementation guide](./blazor-setup.md)** + +### Want to build something that isn't listed here? + +If you prefer to build a WinForms app, an ASP .net Core web app, a Windows service, or any other type of .net application, you can still follow the general guidelines in [the Console application implementation guide](./console-setup.md). You should be able to adapt the code to your preferred type of application relatively easily. diff --git a/docs/guide/project-setup/wpf-setup.md b/docs/guide/project-setup/wpf-setup.md new file mode 100644 index 0000000..5afcabe --- /dev/null +++ b/docs/guide/project-setup/wpf-setup.md @@ -0,0 +1,206 @@ +# Setting up MindControl in a WPF application + +This section will guide you through setting up a new WPF application project that uses the `MindControl` library to interact with the memory of a target process. We will use the Slime Rancher demo (see the [requirements page](../guide-requirements.md) for more info) as our target process, but feel free to use any other target. + +## Define the scope of your project + +Before we start, let's define what our project should do. In our example, we are going to set up a very simple window, with an editable field for the player's current coin count, with real-time synchronization. + +## Create a new WPF application project + +First, we need to create a new console application project. Open a command line interface and navigate to the directory where you want to create the project. Then, run the following command to create your project from scratch: + +```bash +dotnet new wpf -n MyMindControlWpfProject +cd MyMindControlWpfProject +``` + +> [!NOTE] +> We are using the `dotnet` command-line tool here, but feel free to use your IDE of choice if you prefer. + +## Add the MindControl library to your project + +Next, we need to add the `MindControl` library to our project. Run the following command to add the library to your project: + +```bash +dotnet add package MindControl +``` + +This will reference the latest stable version of the `MindControl` library in your project using NuGet. + +## Develop your memory hacking features in a new class + +It's a good idea to separate your memory hacking features from the rest of your application. This way, you can keep your code organized and easy to maintain. Let's create a new class called `SlimeRancherDemo` in a new file called `SlimeRancherDemo.cs`. This class will be our entry point for interacting with the target process, and will define methods for every memory hack our program is able to do. + +```csharp +using MindControl; + +namespace MyMindControlWpfProject; + +public class SlimeRancherDemo +{ + // We have determined the memory path to the coin count value in the target process using Cheat Engine. See the tutorials in this guide to learn how to do this. + private readonly PointerPath _coinCountPath = "UnityPlayer.dll+0168EEA0,8,100,28,20,80"; + + // Use the name of your target process here. + private readonly ProcessTracker _processTracker = new("SlimeRancher"); + + public int? GetCoinCount() + { + var process = _processTracker.GetProcessMemory(); + if (process == null) + return null; // The target process is not running + + // Try to read the coin count value from the target process + var coinCountResult = process.Read(_coinCountPath); + if (!coinCountResult.IsSuccess) + { + // The coin count value could not be read (maybe we are in the main menu) + // Check coinCountResult.Failure for more information + return null; + } + + return coinCountResult.Value; + } + + public bool SetCoinCount(int newCoinCount) + { + var process = _processTracker.GetProcessMemory(); + if (process == null) + return false; // The target process is not running + + // Try to write the new coin count value to the target process, and return true if successful + var writeResult = process.Write(_coinCountPath, newCoinCount); + return writeResult.IsSuccess; + } +} +``` + +> [!NOTE] +> In the next chapter of this guide, we will explain how to use the classes and methods provided by `MindControl`. For now, we are just focusing on getting a basic project set up. + +As you can see, in our case, we defined two methods: `GetCoinCount` and `SetCoinCount`. + +## Implementing the application logic + +Now that we have our memory hacking features defined, let's work on the application logic. We are going to use the MVVM (Model-View-ViewModel) pattern to separate our application logic from the user interface. This is a very common pattern in WPF applications, that helps keep the code organized and maintainable. + +So let's make a ViewModel that will hold our coin count and update it in real-time. Create a new file called `MainViewModel.cs` in the project directory, and add the following code: + +```csharp +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace MyMindControlWpfProject; + +public class MainViewModel : INotifyPropertyChanged +{ + private readonly SlimeRancherDemo _slimeRancherDemo = new(); + + private int _coinCount; + public int CoinCount + { + get => _coinCount; + set => SetProperty(ref _coinCount, value); + } + + // We set up a timer to update the displayed game values every 100 milliseconds. + private readonly System.Timers.Timer _stateUpdateTimer = new(TimeSpan.FromMilliseconds(100)); + + public MainViewModel() + { + _stateUpdateTimer.Elapsed += OnStateUpdateTimerTick; + _stateUpdateTimer.Start(); + } + + /// Called from the view when the user inputs a new value for the coin count. + public void OnCoinCountInput() => _slimeRancherDemo.SetCoinCount(CoinCount); + + /// Called every time the refresh timer ticks. + private void OnStateUpdateTimerTick(object? sender, EventArgs e) + { + CoinCount = _slimeRancherDemo.GetCoinCount() ?? 0; + } + + // The code below is boilerplate code for implementing INotifyPropertyChanged. + // Feel free to move that to a base class if you want to reuse it in other view models. + public event PropertyChangedEventHandler? PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string propertyName = null!) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = null!) + { + if (Equals(field, value)) + return false; + + field = value; + OnPropertyChanged(propertyName); + return true; + } +} +``` + +This is the code that will handle our main window logic. It will update the coin count every 100 milliseconds, and it will also allow the user to input a new coin count value that will be reflected instantly in-game. + +## Setting up the user interface + +Now, open the `MainWindow.xaml` file and set up the user interface. Here is a simple example of how you can do that: + +```xml + + + + + + +``` + +And finally, edit the `MainWindow.xaml.cs` file to set the DataContext to our `MainViewModel` and define the `OnCoinCountInput` method: + +```csharp +using System.Windows; +using System.Windows.Controls; + +namespace MyMindControlWpfProject; + +public partial class MainWindow : Window +{ + private readonly MainViewModel _viewModel; + + public MainWindow() + { + InitializeComponent(); + _viewModel = new MainViewModel(); + DataContext = _viewModel; + } + + private void OnCoinCountInput(object sender, TextChangedEventArgs e) + { + _viewModel.OnCoinCountInput(); + } +} +``` + +As of this stage, the program should be complete and functional. You can run it using `dotnet run` from the project directory and try it out. + +![Our example WPF application](../../images/ex-wpf-app-simple.png) + +> Try spending or earning coins in the game, and see how the coin count updates in real-time in your application. You can also change the coin count value in the input field, and it will be reflected in the game. + +## Going further + +Now, to learn how to use classes and methods provided by the `MindControl` library, check out the [next chapter](../mcfeatures/attaching.md) of this guide. + +Alternatively, if you want to see a more advanced example of a WPF application using the `MindControl` library, check out the [MindControl WPF app sample](https://github.com/Doublevil/mind-control/tree/main/samples/MindControl.Samples.SrDemoWpfApp) in the MindControl repository. Here is a breakdown of the features in the sample: +- Fully automated process tracking (process detection with no need for user input, supports the game closing and restarting, tracks when in the main menu). +- Editable sliders and input fields, synchronized in real-time with the game, for the player's health, stamina, and coin count values. +- A button to toggle infinite stamina. + +![An example of the MindControl WPF app sample](../../images/ex-wpf-app-full.png) diff --git a/docs/guide/toc.yml b/docs/guide/toc.yml new file mode 100644 index 0000000..1ea11e8 --- /dev/null +++ b/docs/guide/toc.yml @@ -0,0 +1,54 @@ +- name: Introduction + href: introduction.md +- name: Basic hacking concepts + items: + - name: Basic memory concepts + href: hacking-basics/basic-memory-concepts.md + - name: Finding values with Cheat Engine + href: hacking-basics/finding-values.md + - name: Unstable addresses + href: hacking-basics/unstable-address.md + - name: Stable addresses + href: hacking-basics/stable-address.md + - name: Code manipulation + href: hacking-basics/code-manipulation.md +- name: Setting up a MindControl project + items: + - name: Creating a MindControl project + href: project-setup/creating-mc-project.md + - name: Console app setup + href: project-setup/console-setup.md + - name: WPF (desktop) app setup + href: project-setup/wpf-setup.md + - name: Blazor (web) app setup + href: project-setup/blazor-setup.md +- name: MindControl features + items: + - name: Attaching to a process + href: mcfeatures/attaching.md + - name: Reading memory + href: mcfeatures/reading.md + - name: Writing memory + href: mcfeatures/writing.md + - name: Pointer paths + href: mcfeatures/pointer-paths.md + - name: Results & error handling + href: mcfeatures/results.md + - name: Allocating & storing data + href: mcfeatures/allocations.md + - name: Manipulating strings + href: mcfeatures/strings.md + - name: Searching memory + href: mcfeatures/searching.md + - name: Freezing values + href: mcfeatures/freezing.md + - name: Monitoring value changes + href: mcfeatures/monitoring.md + - name: Injecting DLLs + href: mcfeatures/dll-injection.md + - name: Code manipulation + href: mcfeatures/code-manipulation.md + - name: Calling functions + href: mcfeatures/functions.md + - name: Streaming memory + href: mcfeatures/streams.md \ No newline at end of file diff --git a/docs/images/ce-add-pointer.png b/docs/images/ce-add-pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..8b9deff25830b47325230c51ab388d7767aa09d4 GIT binary patch literal 14142 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 lI8b17)$4?m#MHAWP0ruZ literal 0 HcmV?d00001 diff --git a/docs/images/ce-assembly-example.png b/docs/images/ce-assembly-example.png new file mode 100644 index 0000000000000000000000000000000000000000..2d22bf4f92816bbb40ecd3eed83739be0412a87d GIT binary patch literal 3353 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

Hf`Bw-bI%q*V` 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

literal 0
HcmV?d00001

diff --git a/docs/images/ce-code-injection.png b/docs/images/ce-code-injection.png
new file mode 100644
index 0000000000000000000000000000000000000000..a844efbe6d44391698f98f171578681560f13d73
GIT binary patch
literal 27705
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

literal 0
HcmV?d00001

diff --git a/docs/images/ce-coinscan-results-narrowed.png b/docs/images/ce-coinscan-results-narrowed.png
new file mode 100644
index 0000000000000000000000000000000000000000..53a7a11ba873f04fae5247fe3298ce4ef93d4b00
GIT binary patch
literal 21627
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 q3Zh@oNMlrz1`)*hwO=saAX%lcy;1a45=rc!S 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 literal 0 HcmV?d00001 diff --git a/docs/images/ce-find-writes.png b/docs/images/ce-find-writes.png new file mode 100644 index 0000000000000000000000000000000000000000..d85ed028c1fdc9bf877d19eb6fd116eccc2261c6 GIT binary patch literal 71182 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 literal 0 HcmV?d00001 diff --git a/docs/images/ce-pointer-example.png b/docs/images/ce-pointer-example.png new file mode 100644 index 0000000000000000000000000000000000000000..27c4a8509f88a95528ef65c2e87c34d0253ec415 GIT binary patch literal 18045 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 literal 0 HcmV?d00001 diff --git a/docs/images/ce-pointer-scan-initial-results.png b/docs/images/ce-pointer-scan-initial-results.png new file mode 100644 index 0000000000000000000000000000000000000000..9406560c9b78782edb0fa75680649c6a5e107e52 GIT binary patch literal 18925 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 R02eC=Q9&tzQohe#{|6i2@-_ef literal 0 HcmV?d00001 diff --git a/docs/images/ce-pointer-scan-open.png b/docs/images/ce-pointer-scan-open.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ca3029f9f7c88eef3f5b7defdd58117efa8cdb GIT binary patch literal 49969 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 ojye$~ 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 T&M`vrZIGm>tVo&A7oYzF-!v{% literal 0 HcmV?d00001 diff --git a/docs/images/ce-pointer-scan-rescan.png b/docs/images/ce-pointer-scan-rescan.png new file mode 100644 index 0000000000000000000000000000000000000000..88e1f1cdd7f3387f448d6755fa01bf00501f95c9 GIT binary patch literal 6948 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 I1@ylEKlbvDS^xk5 literal 0 HcmV?d00001 diff --git a/docs/images/ce-pointer-scan-results-final.png b/docs/images/ce-pointer-scan-results-final.png new file mode 100644 index 0000000000000000000000000000000000000000..b039374c6d598bf3f4643bb07c00f7c1a51acf2d GIT binary patch literal 21834 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 literal 0 HcmV?d00001 diff --git a/docs/images/ce-scan-for-pointer.png b/docs/images/ce-scan-for-pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..2b594b00aa96a531ee9d8a8fe2433c0e7a462849 GIT binary patch literal 30687 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 literal 0 HcmV?d00001 diff --git a/docs/images/ce-scan-stable-pointer.png b/docs/images/ce-scan-stable-pointer.png new file mode 100644 index 0000000000000000000000000000000000000000..03e71eae44f0d65d3bae159151a67a7b94fc8a87 GIT binary patch literal 10371 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 RD&SrO2@zT0av?px{{xOH(uDv3 literal 0 HcmV?d00001 diff --git a/docs/images/ce-stamina-find-writes.png b/docs/images/ce-stamina-find-writes.png new file mode 100644 index 0000000000000000000000000000000000000000..c261954caf028422b29beacb9b3d7920a31c75fe GIT binary patch literal 18886 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 dR@GWE6Hd3OZ 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 UW54gPJpcdz07*qoM6N<$f{lFMNdN!< literal 0 HcmV?d00001 diff --git a/docs/images/ce-stamina-instruction-nop.png b/docs/images/ce-stamina-instruction-nop.png new file mode 100644 index 0000000000000000000000000000000000000000..5bf66c6038e99aafdac07c909363229261db3307 GIT binary patch literal 28827 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%0AN_&AgC;q1|6UsmOU-yO zU+nRria%*Cj)959TES|h{sTxSo@Z{qJ}2#(M~70%u9A93VMH^5KSfs|?oOD=W#ff; zpLg8jWTs{%V!dHyqld)%?#JfMI$-06IZ*=z7o3s0g3PqM^^M@@snRS}W7Rs@e4F)+ zFT3L}u5nn%IwEM8p;-od^OLKJq(u20GZ>d62wFEH)|ZVouXlKca!6&}Nx|nJmor?- zDASJM))+!xkzkz1uRHaaF4P)7`6|JMDk+Wi&&%hej-8B*(mYQ`gYA@)d8hEx;|hi+ z#Y_Iz0{8n286F!xy^lOLKb*t=gVu^cVM|rUz5j*QiZHX{y1DhGCLL&onEe&ZnN2kf zu3bNW|4yYkk1Kl}2Jts7P+Osvrwq~WLy@_R_`*fXh8LpU20IXTSGhl&d3_hoNuE|O zTJF`RRvZ<@c$>guJWnZ8%E)RsJd3HZHbToDt%MR1k+G=YI3ivL;$$)?t}^GzR@Udy z4;1>?^1Oo_?0LUp;m3cLV?bp4*tra%6TU+;;NayZ@3LY#BqCB`dyl!wXrRug*p{>> z2fMhh5aw(}%l+WhtLIH#Kk!c||!ql`q@ z!Mr+t>RZG9CX>w%0Dj!*z>wSLeuSL^e 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 Cl>H0< literal 0 HcmV?d00001 diff --git a/docs/images/ex-blazor-app-full.png b/docs/images/ex-blazor-app-full.png new file mode 100644 index 0000000000000000000000000000000000000000..f0b202c780fdcd9cedee46aa41be3d644f8da3b0 GIT binary patch literal 249792 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

vx&Fj+z}4YHO-sABSng)OKbc3ZV&58q^+t;U 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

kSZro?_6Z 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

p-B!VzIby-@cidnZO#v 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

bsVO#y<|AcEO`D#6_`G-gvPFr~To>(r70 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

{tjy$B^K0U9O*h}zYMG( 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

`M{bzFQ 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 literal 0 HcmV?d00001 diff --git a/docs/images/ex-console-app.png b/docs/images/ex-console-app.png new file mode 100644 index 0000000000000000000000000000000000000000..43ac27e369269f77e38a83b85190505a7d94e83e GIT binary patch literal 4372 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 literal 0 HcmV?d00001 diff --git a/docs/images/ex-wpf-app-full.png b/docs/images/ex-wpf-app-full.png new file mode 100644 index 0000000000000000000000000000000000000000..a536daa2748fbc1d64cf54e53096c93bd2e0513b GIT binary patch literal 138654 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

!cQXDs{wge7e|aBVI?iK4vs*yD>i4 zO}kp`OumXq*m-x`yIEMjl%Bx@Hv!h9U)?C(`GN*j1&C=S!N8y=lJ2zUetseXb{ zmrlRdMIcEMo(J_ytu2XB$z^TNp+t#49(D7vGR^Xe4A#)`Y`Du^vBhkkVl`yVf!jHk z=CxtON*Fz>L2Mi|21Ma#E3p-dE7QYZ8xwdJj! z@irYWkg5E{JN$9AWt=X}3y7ub;jkc_lIQdUxkhj7647y!hN1xyCxb(2amsQk1s~=Z zoJg)i>n!D-Y!$7FztFAQJITdJ_L=D25p-Gv&^4^#23y83XGlA{#UXzpm33<^x;@3* z+0IJ+CHSKJ&93JqfBcUWxL1|=iP&JHF>RCOcKtB%cWtMrTG7sKVLWw$hW$+ z3ZL_v#Yffn-`hX~C%OofzW#=OkFnDVm|O;4`{kN#>hRx7u;g1G&U zkWX`~@`5Ofm648K)_DieFH)CTQQA&^jTvFshCrNG7Irx~|BgF3N_DW?!{^C#)vY&xjeUIo*Zjtu(vUTEEHxBar zE_$_YxhD`DuDiV1peMUCQ}SWC2Tr9w&dVFK9yqkCrM#&NP2v4Hd)IZuW z6xeK~iB!hE>3K0|y*Y8P&wKI(PEooM)A;Jfz%ZFi?{uu!(TKs|B)t~no!)Kx(iw5i z1m;y!=Gm%awPR~(QnA!SElWOg+IY!4lqDjzsC)49V#e9Pn27ERUStLyNMW~Njss2Y z4O-mq?0ne3@b9o#S(zs`AMn%eYYj?h2X&je^sj-x4glLXro8}DMNb&IwPL^6{Xa!b zhqM8alBy*t4<)|;LlOoomLcCvK+;8>s`k4Jr(akBj)+ulQ0yu2%3p~3B8zQ~k($X;+sej^@hgALp zT?2u>_nE(^Z?02v^aHGZ0h^7go74%9Ft9cMbeH6sShx;pTW>3aR4c5%rNjNBZZSdw z;_V>eUxW7IC7tGbC>N6|X8^0|d`O>jJ8BU>cfjf^wZvFh?U`~d9&be!{15#`)5zzd zI)^Fd2vUOX8}B0xnA2I7q%#ZWOe@$?=rZBLQN`zvWPsOcH#^)7`UV2{Hf9l^2DIi`BD2Re|1~{ufw_? z{~Q4>N>%Ae(`G^2K}qeXFWJ(H7vQMPE0ZhL_N=fdIc)%g;bU-XBRV9y!(JB{{)QTO z4_IW`E5=fBXB*10v|*VqC6L6`eUSD3Hny+xbC7aSA_yS0iSc_x$EWKCshOKH(7T90xuzMOQw-PC)u|(+(PB)+OUs{qjabKVLlXZNR@3VR@lujkM zgX*78_Rzf!ovK(daz8I;9nTFMu;1WWa!t>|7bxp8kFZG5x9Ov^-Eq^S7hL0wU&u5v zlxR57_UUt#wCbnrkhou;1a7bg#AI_^ct05bX4lY z!as}l;oh}pHR-0`V3;{TZK|$~mHOP+34F1y(qg`5}mU#9o%Yz?QpxU zisg4a#}AneFf3BLk_TlW6KJ1`>%dd+u9};b?ErVxK?AarrNG z?Czrh-Pb3%?H6Zu?tx$}9lU*~ZSpaopwHLt@m7Cw_Pz;kg+&|X##&ODP2H|jz^T6I zQRWdd<^!+9+6T0%>sIOPJRHqgI(GV+E%d&ze)#O(v5%vT ztyR8DhBl5v_(|Q8n$aTLR>8sakcL;3SdaEPm0wWyx=)jJgF@%Y1m)bFS!e=p)ERBP zqe=2{p?I=9W!wg3H?YV74vSmkqIzDZCR_?N2&2CA*wB)i3ONh_zHdNz-KT#`?r}U` zar518e*;w#yx4U=e)}8axvupd8h%|>^WE)v$&h>by~OK!bWF&9b0ha!ZKwabPgKH+ zm6H)aL!E*5hpBmX7vZJr%e8hW+WPt?pyF7*X2AK>^>!xCmfJ;xP`m4N-pgC9rpu$G zcKTSzu5`6)I^FZyx&|ML?n|*5%B(9Mzi8w2=vt(lFU z>rXcTdG&a;TcAGwp$gXv^g0LN{{u+0AUp;@9Zsq)=1RGm=JMIP4I!~RMf{jbSje@p ztU9=U0l)~`!>Rg3_OSPCY$QlU`}1-+91savDM?(fY;Yae4dm?Y7ofxRPj)%aq-+bf z0v@61XFI#xCSJ*0Z*P!K9L`(X=K0^kgO`zryhgr7lu*5U5dD>` z_$v~G;z3%hYsJ3c6r;En9xQH(m|Rc&C5;F)zbRn*{pG^eLVpW&$L+csWs_QvkcWCV zj0O4i!)*zcA{_$EbgeJo<~Ly7u7ld?HX8(LSZMb0cN(%`-;#&N8W5~{>Qr}JyNG(c zujo~ScUzw-uj^8N-`_7?-1g)t>)p!+*AVDauum<<;1};Ykj=2D>;75ynf++sT_d{? zRSrLqJg-bu276|V;GTh#@v=6%Z4K@r6xK0hHF4HdW}o37oaCj3)Elt%)oSEK7v98J z|Gc(1aB`#&EfTx_zP_an{!8ito1UVFn}PE?%i-LUEv!@5-|DgEdholMe{p6_FNkd0nAvT-J$hvBzGV2>ALslIzOVu6 zn6T^k65f69$e#|B>rE3}8q!$H)UBB9s^QgiWCSCxhl3B5*Zr<)R>uBL)Zu3MG6x!E zxs!m-)X+@t?p6`Hm|6|Su9g)`NlU>22OF&^<RY}lnaM(=qoKup#t+)B(cCg`)<63o2jrDT|ruEynE3g2J}B6nrOKT z7lqJpP_6HHV2X*(H?z55*9kKy?XnrI$2ojuL2(On=BS5u<3fptDR9dr(_aL?s46T} z!m{&69OZH!mhRv50G*^_PyO1ps&^mlC`Lc4t{zp_(ynexcc3M8p{A_t-Lf6*-N)>ips8@t4nZcXF(iAvl0hwkmid0r z_oP|yE%o<8InkKD$8A%t)1>UCbZqHBjo%Rm0W;}7^h*v zYIWuc1bY!97}%~zPd+-)aD=J<@O7J0bR)d(v|=KCKsG>9`v;psp?MR(-U|-OB|}cnJZF7cMo4JhxX6vQ zIy$qwho|aX)Hu@_47{mahf~nh<4BG2l{(3_{g3v=LuY$gK{W)ficHnlIB*HIT~afc z>Z1H%{h2P1Y^L!eL;1k~%j0Ev8I1d1&I1P`szFJ2D?0(p@u>`-0K}R)iyZKIOrYAx zOmV-}Q2qntn(x=ghsCm*zbN|M=&?{t-(wAunrh-Ybz!7RF3d68lrKd0890$j2X-#> zCsILC4Ofs>upCD${tq%DeT+`2YrVUoIC2~G)pi-&!XwrJEaK^mAGr<}nQ+HD{&pm5 zA8eE|xEX)GRMUC4ttBZy+-)DAqX(TB52ZMJwt8_y&KE{bUB^ED}+en6HP&(T7@ZUDl$i;d z&+{q|)xJ;o1YOh}F--Q;b0H3EHrzem96&mEtT3L(cgefU;V5SpXN$IOn=V_gTj)F- z#vBnXT{V@RcE6zU z)yeG4>Wu5ms*0%=D*}CuD{fbcd1Y&kq{bbdzy_(-QnmYJSJ&d~NnG#j?8+uO{u&ua4Sc9b2)G&J z+o z5}8Vy8+NVg#(Tx0hGLWR{&p4wZ-CNF%XPzjG_9tY9I;`${A&^)(dMa57vGvz(9qR} ze-1#c8m$$M$xJSHOEXK!OI=yzl)Y$^gJ({vtPL++-M|qCH@lul(bd_{-*$BN)>o+Y z^We(D`<@0aOzwB$mrCSIJgF`Eu46tQ!y5KUx4pi^I~MRv7p@N(_?6rF3KVd=)_MY# z_bxTRTXOM{2yA5qTiC%jwf=N2t$R{FIb3t2Fl}>frO_JIk@nK^JO8bL=C(d(sa4-0 z7!J3N4wnqP%}g=Z-C#W3_-NJ4<5=Nsr0V-f2j8}5{jhB91^#G#^%I$%y?4Io#o9WM z&r#bmyv*;ITh4jQa7)eGjQxv+Y)tYo@>rM}#r)bzD(aeQ{Iv~h?B)oLc&uA2FuF86W&VqM<1h`i1Dw#-N1s(Qj| zu4XEmeWbzTd$I^K#Ygqs?98vMD|RyNTrI8E`d!;!;H9oiXIZRtH++rDo|lKPWHPLN zX@=!DV^7qZijYkWjg1KeHQLx=pzkB2Hmsiag|$gccbq2iJ5#bTyF0Ld`7v=9cGYqh0k&)3dOtPn`0rTZc$wxYHGb%96dGH5=`7m_ZTlU zbBd<1?_2B{4+br&%Rw}0cB~n8G_U5+az>VDU2oj_nAOtWHrL}nHdJxTm6mRrrQBj| zt;odY*sQj!4WCK-#p44|R$q6eRb@H6PRD((+rTz~Ubeo*Pb-JB9KAVbk3%1<4(>`h zcGBB>^t5_zaoDI&3|@%5Dn!{|Z?W~gqM~G709LoGtybqc)>)^DYr6?R`sJR5HP`c; zcQqpT)%n$0HD+TxA+PfG`Zm~gn7vK{GF($lq_t-x-gylc@k>p?f@s?(pvwH3otbBn zs6^&5Ew+arQDQ9{RlQ2k6WH($xHb06czE0}%PROdQ|fDt zi_aV9SqCvs7w+Ok8?Cz#5+fu88!pfH7XShhFA$&J25+cX{}uXRp2K^Pu{+7j_pv6- z&pR0^NIE|cG4vE%KO(<~H%>Wt23_z6YbZT&gJEgt7jr;uREx~h-3K+vb}VwK@0boY zCmmgRjP9dD!7wd!&W)XCnROjP8wZ%!8EPQz0Ny<6mq{p7!FFo%KhBQz2mz355@zeK z@*c?Q_VyRJwzjHw!K+UmYLGC?sjV9~IViWL=Rs{!r}Mn^cR75%FvQ0!Sc}}N1N*Y= zQQH#>Yu}(!S*?Qt-ELe@*n3-Iqilnk03asdDf6UjxsAg#h<{F%5JIF>{5FfVnu^YD z+A)a7`3*3+S+)7ic;l+&Qz^*$~>gx`UANkEaB`+qGCY>wWr41Ca|1;~|3-Hyc z$|m9tUkIiJ*%ok9RU_(UvoCLUq;|G?!Z5q!Ju}JqP6y)ey|AYqw>ef$xJ|iAa6JyL z2G~$+ZnYe%PMEYUTOAzvZYJ;fKPAk?{aj>@RDtpWTL>+VCIV}`lsm$cZo5xUs=c%| z^d{>VZ_h0Cs`X9vp#E8iU*Te(BjXyZkUP!jnI>=L*LL%O{B^iOcG`M~r@UO0C$^7p z+|+cu=B*mWq*=ZaAE*KQeU5L>6|q4Pd`%UGGuFV2hY7tdasp1zdYO31d1q;CTx;Jg znaF(zO^rF(eZz9BoulTUaiFM^L5uetIOgmH3k7QdV+x)l@I8^JtM&Y3$EkbZIIR3W z>z!=8PThLg|9@PaML-;F)2>5scXtTx?ykYz-Q8V-JA~kF!9BPK_u%dj+#QAi{z>wF zXK`4}itg!otg5cMtGfUG*ohZUVYq*EH60dP*^V7AeyC{2jGF){{C3dE^}w^`G)v3d zY5y53gMU3U7O3zjyMb2KdeYkNsX)sZwYr0enJ2-ld-JK>k15c#Qk!)p)4HQ+hTpy0 zd$;@m7-rGD@B-zWYjJG6q$F{mty2-yDa5Vf{B(3Np$GW&d2k`5z01Q(V$c%3`?8ec zY=Gol{0S&maClsO5EXZDeXGOLI!)&coh5Wd>91hctwo=@Uy~o0$)Vb)>+mZLyE*mp zZDXhF^1Z3B?XOZ`_hg4K@MO|+lh+^*p|szk;*uMXx^2Re$s`Hlr|dYv`?IIuQ5FN# zC#B?Dqy*2W{TUX|r-K7Eyo3Kh8^7ITR%yC|{n3(U)>|%fy*2xMVXtjMvRiVb$D~%C zkKUtH9ae^Jy?g7&=i+OTRQSILRE-}TC7bgg6R-MkjSPJFE)|>NnFbRhm z*dn~QW>TPO)s_+Jdge`oiT&wr%s)m6hHAmTSX*0S8dequnztGLrUm{05yd(f75K6c z3r^xxt)>hR(B1b9h>|d|N#W5z)cWUhLw-a=wakAB0_Elu9Rb?vMbPS>UV&q$P+5aY z#JywaRph3FEwC81R!b(21GLcp>4^J3>_Gk25B`FNDmzCjMOKK0!JxC`@&H7r?823< z%H%bniH&pDOw7P5_zNlvcbV%rmP9?8wVeuD^bxWKe~vF^9a!67x2v~KKh^_T&<@n) z8Wsgs#;WMJz@xmf0Pz|#y8oPf_g+02kq9sGe4DWbT86sxW6B*>-GEFED%ySd*4$v! z>}KibQD-6t?|8|S63hSi>lfIh;_?7{iK+~qbpnu*Ue~cE=g4{Ce(r$6b~0uu%0_IE zN`j^qO%J7I)e*&#qu>Uh@BA-FC=jlt#+T9?Db>y47vReR_hNEQu71 zk_oMNo@GLt&=*PMA67xQeaUHe3qw7f7!`S8Vc!YT&5J{JGr1$H1=eoWl>fnn@n5E0 z0~Y;!ioDN6ZwD-H@8l%jgDvF^H_rr|znYECl{7kfFl+x$b0}FOm?yk;D!16gt2e_6zht=A~ zdxy_Du{Uv4#C}fquwC{){*L_=m`#OASPZ>fB9*^RMB90J(G#u zyyG}rvw?!Qv)Wrx@Fhc9xxziBmOGr{sSXm^Wrb}}1+@-Y#Q zZ@`s8Ut*LRK|s{h`c*}myd=(vvF2|Q`p(m3XbF-um__y$9`1Xu$B~<~DAkC_&s#wCAzq7UbgE?y|@X$2R=Q*aP>n)Q!%uTs-Pv|+Cug9iSkVpYK zn_e=8!nJb7OPn7UDOK!1Q#WR((^^yAYIR>x_*c5$GuL1eeK(Gk^$^(4Gn)h_ zNQc(v=DEkqdI-_PLi%B^-}CliUNk5Qymx(=+yQX2gpZf*mI+SoQen zOL!c5f8t&}SQvILzqj%Ct9mM#=Wppu_wzE=s#^r!KB}i134kIph@zFkm2?{BYGe?0 zL9Y1mNepZvk8s8Xp@$_6;rdLawXUs6s`cZrLg(SUSj%OA@ZHf0v}4CJ)c&uWwpy;? z72;_BN4+}{bmP-R)k%NL=Id;}M@6!1k!;c~8YZ%?&FH0TO46 zMX}T8WKDIi3?GdO#c-buw2?}O`>~&}s5V-C-^h{-z3NkzTKK{#(pd&gd})5KEad*d zObP4mTXEWJ+sMgVll%uy8>M*F{x}Gm9uJG)ll9ZLKdVu})=Wv*n!nDx5h-@ZZ#k3E zF=e7(x*@G^HM&-r4qyJbPqKt@O+Zo6NA{>b`V+O6ajT)&VuaigQ8H-|+g^xAlHHlL zi!<<4cRnR@F&&H#+I6Q}zDhI@ARUBQG!xiOVgYR4?5;1iaMoXL)3m&=uZoDfMr9fP zOHUDDY+h4sx*cl9E+xd5+7=zV{jNPgzFPo_u<6i8xC#y9A%z2q7bcP_q5{KB*DOIK0cY&~laqYMJ;Dqm?^Uc*f&qDE^;=67Qr`ahw3 z4&}M|@2mtXoSJ27*jTn{wwUxQF+@C7$QR0yki6_!`1Z3v%X6X_Mv*zCDOh3P|H-YA z$of(?e_l9->!n*eZ$HO@tg~FE#fD>}mTD)S4~-Fi@tqE3&mkGzD&|lXMSaablUfn- zA^9aJlu@A5B{DPqK3wl(dTKLM6O>ciRhCnRM12@j3s}&BRdpe6&p#WgMh`Tv{KTO5 zC<@nyA`y&YY-I})S>1l8LmkiiBKP6c0laSlBIbj}KRba#!i`#p;Rxj0&PZ~2;o&d0 zAc68HhK~;|y$DKmplo)f4{K?^`S&Q=A9jZ3N{_|sqouzS>vQ;|yg&t#fNg-^B}BqH z>|dunx96wG{WAyZ&@+~U^bclE~L#*Uj}~IVD2`+VrAT0DWuz zft>!IKchz2Zxg~23JVpR&>oNGND9oO3#To3uam(dS5uFjF5%}?<0uH+}j zU?vq4dmhi0-x=HFaIT$TSAq9DKq-$(kkuwWKHMiQUr>y7vCu|2&% zGi|9dxnFqll0A(HuzqDv*!Nu=Or0R@5YFpAcH}}X& zC1ltL()cml25s%=PG_mpfz2UnZfJb7w!S4>0ph>U2qtLO<8(|gMsj&~7Z39C#raBp zE-{6d5TZDo`x5d|D&h=~0+5WyyO%rLcju2=iEClc?aWklAbfG{-@gw&dA>F^r?&Z~ z^!)Rc&{8q=6060NAT1%=r|d=a8YBd&#|s2=>443_7Zdqi^Rn6eIC}j@ZKcya-ShHm zSx@-#~bE#c5JKtM#kMjGr^ucuXEaO*YCDG z?qIZSGuZjOj8yVDap56qe!aG9Ia9Z_OW>)hfPRv>S>DtQYsM9Z^P>$hx1yx2I)b)#WYPn zapfFjgytz2kwAyN;G;;ZiO2n2iH2JC*6g`RS6gryyPZX@APe{l0$X);!3CON6NSdp zFUta>%SWw=ni1kv0$VEfreg<}^AEQP3$KlAmt*=OeOju&&Osuz>DN+_GRfcR(Y{Tf zpzdCnZ)X|arDdLI8UWTyUw#_l`a4^D=Sy{WXeyH1=yo7oD4F1^m(G>oNV|v-F}bQ`&F?{g&ki(K z7@RnfNIHm#M{*zj>XmGW0=}PvQAW_1oLkL<#nP9M8%#)5X-c)}dQ#vbborL$x^d~N zRj{!|U>ENWoF~u8^tV*n&s`Iq@_fh*gPr;b0CNVaEg?JFUaXb_@;#Vp%)gBV^zwd~m z7(O|p>`G`|HDSiS3=GoEXTp-DG#>L33X4WoqQSb3Z*D0ZfJV5`r^)?oR_vlukJ+3rJXhoWU9P#kZ`#MH(0h-i{h>=3bwZ{bQ7VmE0$*`_R zZRsNAe4rg({OKVwEcWj&rxIy29>EI^FLZNju-dnDgRlpxSs(1XD72KXKSC;pmtzKn z3rmbu_tcjvic^<>bU%^0?R4p|7*!x)yNgHC>zC6NUFtuD1IpLIkj@eLYXUbr?)Ip!yInCcy z;EoAUa_&?9m|~&U4Gn{fnc*(NQT@-lpM+?oEF9~o?X>9s7HA1AXi|1{lZPEMVOa+D zbX(UG(5K42exhS)#bHL)JrMwLj{`q%Fp`hXo{x=rk5>A~9!RBx!!fShFufXc?Zl09^r#gBsT}ik zKoLAq!=5(FKb{rJ^jg;XtDe?i$`4fGT8bl`RfTcX*AhqIxhZe(X+N-yplhzY_8|@D?gL=l7O&DN}2{r z0cAjyL@OCi95wnhHA=C}jT@ZC2GJd%Xq2|;|NN{VP|@_k=fD!@9c>JdXAqVrmtpLB zFvZUZo%Q~l)zw}|Z_i|ik4vp%?f&B8ZCvm7NIuzR`Af&aP|t>x+_lI)Zz3O9V@f>GcV-nSI0$%{&$VL=xf^^kEqKEwa)yj ztA1odGx_bQci_~P+nju{5@<90#NT=v`qqKLXzKpbvgvn-pLuUvI%iASF2aoyn<`tp zW8~(>i;>#DV#1w;%y}7r9wdiq%dK=X6?qu#J^t{hC0@1Pg{<;k&wg!Z0nq6s`GG_iaU|Y)a()x3r#43|fP$r~SvC_g&P})9!10*#gi(5w94y ziynbi-@PaA*==2RX&MJLSCYXl>rZUUi$|CdyGR9~SoCAbo{9*i+2A60o(!?m zQi_4S#^AvGu>Li?Sf3P+JWz$Xt)Iw z-n5?=#{P@ql@XxZj~_wYAtUaf9C2R^LihHdPXkozQZ{gAb4zF_$*;ry26gS%md?OF zJPRjwpanl!k2UW3{_AKnFd%G;z^~WXL#rKfS(DG{ghYVBqLf4U~ zS^#%4&tl%w4?ah|nyJIN9V1>s`NiR)Yo&48<#;Kdg+Ui#;H+cR{Q?ko2hNJN$q2wk zaKw{pl64993oBYn6R-fxAq{iMH9gKy-+KvR$X|DLeR-1Ea`4mmBljm3dT9{yy^d7h znpIP-r((RU**?WOB@d(PDz=u%$2%F?&c~g^YNt5DO>2I$E1Fxa<6*U_!MEkRcdQ#y zK!5i+od%^R$xDf7TOgsqUnNgL;bLYISH)d(L(9SD4%~43*8bS|6N~1b^*iW_6B3kK zN1Ssq_Y_J)sfh1dP4sCJs3arQVUu|b;vM9U2I(U3kcZ=ge)W3nw-!9Nt9_GUTcIyI zJD7h|luy~>Ii~7r{A_wd1gGzJ=APZob%MAy`-5Jfa35BY+(L(Zva#`?P#Y;Bwh=;X zt~^n*^At?uCCGn+sj=kW31Nv0?oX08y699^9Eh(ACzn(}r9NIjX{Z~JfNs=pXM2&X zh|CxLFHR)D!ic+hMR{jTr@lAquyT_7q;s;lt#wP+1FNY#c8QstflsaTtd8vyzrSP( z3I#xa1Wb@*mV-K|)yCf>tN{h-3N;6%^HUocS#3LtfI;L*B zM!}ij-s2IF>#ZqYgw4Sr%VgtthilRWn|7@6K4b(cjss-+Gx}!PFpyKV0c}WL1zXPBT%P2wu-6Y=pOZ^82}B${*{mHe1T!}Z1wh; z$TF;lLNC2}Ko?{dctO1UcOf$kj(5 zB}rgmcS7s|ct6am2`G!ip)@Fkl|VLpT^O-U>Z^gmYlndOg*AYfAjH7!FZR zXs7#!_NzNv%e9Hw=LTKcd9vvJmY6O(wHPBnodP^quA8eWjEDw1qmu`NNo{zBM3rj< zhe3ntwYj7FAhlEGt;ID4d6gr?-5R zi}}Eb`^=Myg^AVmpM!wya|`&v!)jN9&#{D%RrEbkkV@1nYC?w>D17$I^I?V ziIxy@ji1k%ud6%2(%r8yqKsh-Qk*10XFTz5dx2R~Jc}rOxz5v|#7)R+g79A$?^k28 z&VKq9$1D=@b-?480t5SCa!z-0j&0*ycU)iOtIaEdt#-f{iAd9U4u=-a?w_wS4am8z zMdInER0{K2s(%ddOi&a$$vjF3%x8$4(K2oPXVOJ+)EL->1HZcGv>rWTmkD1;Sc8yW zm+6T^_%15Vo>LuzWgRQ`mnr?76Ayoa_X5>7tN=~H^L2ZrggozY5Nvh37Vpl#oZ9XI zmGn47(8rrz!Ugu|7>-I!y?XLbuW#0_7XWVri{EW~9&9NIoE5&J*^_rEY8x^@mlLw zd}u6dvhZ=jZOfrs>ty2`&UzfaVZYZ;W-&plgEoe-E-gy-%l}Lq$TcYa|BT#VsnB(lSd>;E5VW|xngwsTA`{}NP62mU zR#n(7r}zU=TJlB27S@S_s7z!8zl&SC_Rlh1pC#l8_oMSI6~`%V3%ZC5QFS3iS3@j` z5s#ZyFP-n&b`I7+TF90nB<^NP(ZMqy{ zPc1El&)v`5jg+lp5_o4yOw117+}qR7g4&+|;JUJ{6{=>3>(ez@@@922Qv>3g?(};j zYM*LAREsK0HHuC=5h9s!*$o4lu$rc`Fml1|S~2>v!}cYv547#!dWw&CQ7gNPkKvqg zQBQRp+isi2@0(_l^En9QQpab!xXBz)8(}o|3Y5ANQo?ZMdde~oWK6I)9{|E1HL*qv zN#nQtZw0y)xp1h%x%&uu_6CmEphH@+9TYm}c$@-V$v$d++4DqibVzOn1!L|B9g+Xs z1%}@XvVf5h(YX>M{&}&=sI6te#qkyBtVh4Fw4RnZ`pJ4I9dDLf5a%;x$n<=VIM~dq z+#>md7~b-K)g$4ju?z$DTGpT22LWd6gtl$lq2&v1>^j78lp)G&I7q3PMl5r)V=={n zG;(Co$8o{n{!%gD7A+C*7~CI`oBueRpawPI-9@E8lhgyq-!amG>ljwi;yMP28Ozg- z6Z*7aA=Je$L08({W&aMU)>R{wMh;s}OOOMQ6%Z|+DO0?-<>iLQ%WD4GE~Y(98(f5a zhNS!7!9*-Zsczck?tdhuA(#4VD)ZL-Igbp($kkYI8Tg0tz7-bjt%!1cgw8d2#vX_P zwfo=m4Z1?!$HYg(jeEu4+Z&cN1<&ocv1c=4Cs{D#aTFbUxtX!g?o|e1wK8E1 zH1!??KJNpdSgfEz>4G)9W#{bLI1Fh`7=XT2Y#b(k?KS~M8fty_J;)VwC~sj$%sgK8 z`oTF19a%~)FlF+QGtW|cP|30H-n8mwR#*7MO+IR|&ME7A{|g_qMqWPN(^B^2(d6}A?#2^2 zAcE?5;Q@lFMHuel&G!YHXKovfZ631(*Pby2Y3{iHeGj;WVmtVeaEh-lK(1)%m7!}k z=kt8>PY&GUm**vgEL%#S1(HO>Zg{p0{P{J;R24NU?Y~z>go?b}Z?AN&qwI>gICWxXfV&IZS{8HV#|&g%4n)lbqfS-^K`UL_uEvVoG*FWau3`-3@wheZ^R zeLvp;8}30MemhvO$EsoAH=MTd4A=*_l(#e{NP(6i(Q$Eb(N3iIf*E@@!kvwX|IBr+ zE@n-ny3f|2QNffcM{;_cJqWvE>CiphP8~+x_whva{G`swfXsvf%Bu3MGA)IHWj35 zn6;54+ReeQ-^$sx-RX!pxAY8WGTn{Xx*oK2#}rMp)WH2pQ3MYW5ENS%)x-Y7??Brt zH%O~Ny39Ghu=NwpWM(O6BMQ0W&jtNn+w`#K&F%0s%cBkK?$2c)F3<9AM5lZ{me2kD zk`rs)`Qk3XuwiNG{x)jJk`Vv$^_HU0;JyEHO9H>jfBgMPw`B6y=8O6o4Wx-d)L(O{ zhvp-ep9T*%{=?)7nA$v$KQm%B*%IC^lunGPT0mXwczmi+K(?vU6saE;>3*P3<+#ui zCR@)Fu*>sgyd4+#ux9w4g;a+9_Es8Cg)=D#9O>TzdWvp5J-?sH4m@mLcHMVTNwi-f zcR#&!TVgg?SSo^)^s4`|7|0k%!u_3iui`wRGEy7PI$`?O);N#NTdg_%8YU9t1ABM{i3`e)2R>h+QU zZ%EaW8DZq_Sgr!Yv!)DNi1UP^QcQixduhamG|$j_S@5qOTE-LBP8<%fxxPouS&r0U zL@+`vT*zK;Km0&+*J1ts>EsV2E*OJW$nKUxJy}2YCE~r0d=~ObPilNFBIeEFmbiEg z+)JPqPv!fP>E=HvU(2G)7VsL!^;#JrRCqSzs|d_Ey?nl{W$XGwMs~lDjM)9ctXyWy z_#D@SVp@2K%x97j)JS47nISzn?k@z%RO@^gXdohe1b3RUvs|xB%>QCVQzcBocP;E& zdbJ$onal0}9{+VggScle6jOR;bR+1THe~y@G~i`e(XZ&ncI&N$ki;GOs$;A3lALz1 zGVN`TxJml2rR^0#kRiKZ&KcAEF=D&>JHaB6#2Rua`#5>rEM4C^WxI1li&w)NP>lRl z>o{dQJj-znGcFf3K99_6-T8fI9TTI`V;A!s*fB<4NwoO(g#5ZZ*>i)xhNXis0a}Uj zj{&bemDeB)U$~1KMBP8|xwdB6hS83iCF^Dw-(Pn65>t;{sdqs9j9)GwjpUZq=5`yd z8?B~>+syl;`wQ(zHlP1DezuaimKO99Q2EfxrS_5nN;479!n)@{MFf2x=8Vnr)1Zms zrKC}t#1jQEpq|+03cLHIsolNxjWF;qs^?nDgOT*Fl6M(-qWD7+?Z(rohJu&=pxvUK zgE#8Smy89ao#;AA2_ik~R|)(AjAfweZwX~53Z>+>({6ls4QLSfTr@4)D<$H&pFStu z_ya!mT$FtuV;EbvT$_l^CV%C=T|V?Qv|PIiJ&-Q}R~!M~EY5Y`<^yj)NlIzB^J*a_ z2Le#W>8YdkW$JzK0$1nn(07x3u-+dm6DN)~gOMvTcD<8k%Q=zUHrYvIOTkS+_xqHu zjuR)?U>G3fhnY{TgXuO-`H(LnlS;A_R`<=Yp17|sW`PX8AHDb@O*7pGQS)@i)DZ5ywnL0nr)q~0e6(*|g2F4O!$)P==*uvAR#}u}}A(b&i1OL%F4`?z~!cpRC04QBJD35tgNc6&TKZHpUbd z4q{Yg6IvYOME@U0L#kHG<2vL5={nSCe*r%mOR@H0l3Mdp5+QObZvMW<{oZ@>q7`j2 zUTskas**bZF%3zOBq6(L*I&8k=&3Cj;9muDwJgEXuKlZo0&a6^^=oS5O6F$V*wbU7 z0fl>B+#mr#QLhvGCnLlbi~bdty62DRQS!ag;V^>fbn1Daw zzvcw$!}}mhG-Tn=nyC;zxAW5$DoP7nqH4#eHT^Uu{&w(dTk6GAV!GBb*pl#6xAeq7 zdQOU=Rg>eu-$Mt~S`a58Qqu?BAy?QutE_Q9A^Ph9yh+m5Xz~|vg92ItcmXio0}9*{ zkjnpWFdxa+P*vu1WSq&K^&HXnR(<7H&Jns)#UYj9j~dtbwsA{)GA37Qf`DSPpj0kr$zn03$P(dGO(-I z{8f}IX&VU>GRc!HPNtA-0rLo)f!kOMj7=B%{5n26O~JSOi+*iwTA<*k`^8i0vHN}L zyH%hbkaUf2^E&=p<OuKxEY*m@o}rFDcFFbtWoVuN1|#)rE*P(xBC&hmfghv{V_0*M$NBghbr)f z%JvUTXrN%C7?B(Q3`O!eaZKo7 z7QOD_Iz^+8?{A=|?7Se-N*Wj8qg9M-;{6IiYHg}2L>J&w*;ZG)7T)0fa^caHzhM%k z0^ajL7Bt3%nDf6rAk-H}kmqn}*qm8$41u-IB}c_gK_Kd~a2`rGNZVtC61n>%8O;5N z-SD563u#bN<0Qro3X<5&P56v=Ih2lg74LL@<3Go%e9p)rm1`yc5AkqDa)y@C17$y$ z7;q4kJ$*M(-*h_lY>6O$1iyb+1ER$Qhu~!3wf%FRVKeC&?xKt3d(5Mr*l!&xe=%jk zyG}L^?Ok~#`UcSRQ^aA^w+llWmT05=_aNXCnUQk%0i@_V(OONTspa!Og@*7jIX7t3 zT5kx&7&Wch>GJDc_p&T-;}g#@llb{dQanT+ZvnEtgvR{MAydU20iX(?Xgqh`Kd$ z#a{jBV%*)HCcy3yQDuDErb`;%M_^9*&s^ogPp?KNl1)<9kKI!WX0JMCE&v6R8r|P+dmJHk& z=yC0beh=s-@1&{+_t|^}1XjtEKUW6&T3#JpZT8%;9dx{37(Pe!JQl#$0v=S~@2I-( z=>m?3bNshPbr<0hoGymnhKPZKfl^>h4PSo#XO9OnPzUn5-uT%m~#EOV+mE zuXVo_^*kqbh5VG5>^_Ovys9UDNkZ!N4Kiv`t$GUI2DKUle=o02a$n#)n+X^A_rRsX zN2EZMLYE|^7$$r=upKT&#s$ z!pQNj+yix0kN{NQ`(DLhS}+g207gk>#f*b5SgxsEtE2~{w~ct(HU!j;vHyPDdf9qM zHoRV}^+#2WaA~1q-}&6dy$Ht|IS{p4gk#oOD7(0c|U z7dI2Wrl#(nP?waXp;E5yPM%)$tI(D-4&HG_sw0vpg%b~tQhAl|J~QV?<7q<_m;d|J zQF+{jRoBJo`+@xXZD6VPSXTG@UCS#|;Elyh_sdKRFk=D_G$BW)JZJmRe@0vIva}(1 z*TkY@AEoiM_5^89=A!H(@W5fWL+EYl<5 zfq!4YU+uhr__mV=qA6N{j+b%xnEkD{<=^@}cc-cA*m(<=rp}x1Z&L5OTWfEs-$Bg@ zry%ZM{^(=VV#c2Y7hBfd4s8!k_Y%7kIBY2Qc1C2E1gdqA4#$lf%ud+N;yLn|EE+&M zujvrFYN9)V-l3RPfk%LyVja>K!`H1hfaBYY&<)@`(OS{uuRG?71jqU7iB!!(S=L|g zduMOXcaDpY0@37Ljb8HKjD>yb7$`jld9PI3z6FoBg@5+qdzxJF-{xVUW#5vx&3>a6 z#m#=rw=#8G&=dpT2CE ze97QEd;`>J!lE_aT?qH|N5M#F(I!-+`sC%8Xk{9v;D?BFSeTdpRq2$q0!RiXvOt|n z3o+2?MH3yL%x#LDByznPuMfBKTUzL9DX?|?>g|@`7*n$VHlw>6#5wD}gSL>eHM_3p z?$w~?V}s^9tc!2~w;|>gf$0L z8oiWIzNYc(ru;LZHG=FMH_1_2Yz~33DJj^`iHoacZ2_wUK{8cidKGa>aEj3`=PHO( zP5)V3NLZFAZm=166Ev16TI#IZ0&c=Yf-nbzwm3>B%geu827 zzJ54I+gl&m-lJ*YUO*zwK1=ws*Ju-A)K;fNA{an=ElNzHF)PLxMo-s8ZQ$|B(Q{j` z*lRHM=h(tzl170ExZKMNSh-(Hy`d=3;+VD<{s=QyGFfe5D@zK;%}V0{s?t2Ry86A= zJ?~mxg^s~SYSXOcz;KVWUJa7G*FWV63cf}m>+NC>Y}Zrey*-&K+D+yPA$L<2&1m?? z|1r+aJVqW8tv<*1OUXBXq`w|>Ifiv@f_MrM(K1OhKe=F_2g+xRKDcv}o>E{b0!z6V z8#DE7p%J1?JiydKhDuE*knNY z#j7igQc2f`)&KhOcZne(2XJf)6Zd+u3oLU>5Mph{K6A6F_m zy8oxqjuyuqdj)9k5LqQgh@7rN_ZKX6QsvT^Pj(PFTbu6)tD7+D{8vKE2%{iQ+?Qhd z^$jHzqaAO!TB2dU)>422|6~IzSDAv*x_qzvpgsz%nmw$X3J{KCIhpY%8E9j5y6*6fTK^?o*z2|l&Oyw`P@wZeYm686sXjuP$|W~3K&F|>3-zol7AErG8w=#GZ$5%-(c&JmMRsoUV8E_tc32GrAV)8AZQ!-!N6OdM;SlumVVNHQm_jiiqww zs1z|X-jHluJpG`bahhyuiYd9gl4)h011vo@+^@YIqJ;tU{FK{jl$ z-MKJb`|g1wi+oPhOs(BoDAK4*pqCz&;v0@y)NWy`N2_Jdv9MPk$}W-{#z_%rpiH;< zk>7|$Q*+t81@F&kM;bD5B?`r5kft(n@2=?B6d z98p-=wV?d{6!7mRWz%r;h2KCg5wPKN#b~!9QOAN|iwr;!VFF@N`1g$<*H?a=_ps^k zE6`$a81A=1>O*-`!z5yJe}gE2^*8xoIhx$Fb-Vg(#-EM=D<^A`gbg+1Z>}UnfEZ!B z*L&dUPvg_nDGCzTbTGm=BE(woWO3=ngb9{6={Yc@`k054Ys1>%BD_IriUT_}tDaE?8U|vb z)w76~2~uIEwR8F?b~A;ht_#P{K!4{_d-qj}1NLFuDBaKBt2S5ZA;;ul2VQ+88RTs4 z3V4vv&MC3M$c~94-;PHZLz(z0CqteR{7##`un`#C$$5a`^HZe|*?Kmj;l_MPPm!H* z%rTC+@meO${c1%nGk}$NlbOBy129Y@@nNlEdNsxLK^{x~SZTfle_V!{rFIB;!Ccjj zf@-T3!jp|uMf?-O4Jk$`NwD<~fD<1Z4%;WUkn;OQ8hg#v%?xYo>9#3co-_V2it%@> zYRH8#W437nm2$ZH^D76g`oW7>!4`ZYGi^EHC7e1Jb!{b}1|*o+Q4q|h?knyf`(-%+ zqk$ojptp1+OJ26;#mZQA%r%QuY;zSzQP}*MxN=eOV6bF2CSi5Gunl{%{{!BS2)4qk-9hr+-veekBxCp7C{!yzx-65tWNbsjiBkC)yQ;*Ychlw!jx7~Cb z56?pkiaUNXc7z0X#-MS5FOTPZ(HME2i^rd?$0mAv1ikAlzQ=5WusObG+-%sb3rjrG z>{7ST+lg2d)fukXKJ`T9lX7J@Jtibd)1jSMcJ(ie!R_j4A57jaY+?k`-fKZhIKeqy z-!**nBxKZ&aulYWDbZU7kD)xi<|zn=3ydyN0yUd={0u;L-`L(ScnjWN=skl^+>>Q%H4F7CuFoK2T-Vx)0U zD^~MUJR4!qMaCx_ZiVR1tPi|gv+Vhc#X+>`Wj!$<#}u8@&qeF=CNmdSl}w3z8U@hp9#$tF`3*btu*9>e@j`D#F*f&9 z)kjD*%#BfnUiXG!g~P=pKX%lrr`ojSOh2zu>F0fwgPo1Ia>GTF2Im~Ef&mWxdV3JWz--H z(uE%)o7%;@_dWy3pxIvE+Gr`mY@O!@y-A#vW&v!R_U}YSa^L}!7oFHiz8yEj={!RBxA&zyUEA$Vb&1SX?0)&- z3P}vf9UfBsRVIJdm`q`0(&R&kKxzEMbj)_gX5+8m0KSmB*l1va^oa)Mng@b3%i0H~ zvpdeWFLGQJ!bhz!l{hDnk#(vo4-?dM%^1EnFogxJ%2pLrMMc#77D->}cy&pDIbJkb z1&w{wO_Cn272+*+>vaIm>mK|LPB<|s*u?6gY##ZWElzZN3{>N)Q!Q3pYJ**|_V&YA zg&c=_8HUXRvc&$O0YWmRPbT|0jyniRLXN86dB29ul1iE?8Qix^534_!GLCgklD2@e zg!Z>z(X&S-QIEkv%hwp7K0ZU*8%s?0cT?fCFvNnJc*qV;x6eeRBd(7iLDn^zfpuj2 zx7Wl+;gFJBJmS2G1(O~m&&o3X41x8KG5D4Gir@SzmB0%A1ZB*adN_~;k7gE5pLCa| zx#G95_|>E@*eU6Wnn_bRqAoA4QJQ!M?;Pkodb5}o((g&1HNnH|;VZf}8<@!{UM~oG zcg=0Z)vJWh9*u8Wdy@%ABF?nuo~Mi~VtCFW6Z~$D<@EGjC#2b4zC!tiZ8Vxlgzk>> z-4Ggyz=oo5;Q42sXpLCbS|7G^(kOB9HhKyRNuUy%dkX(JcaWi?2H)5V4V6Arw4^vn z+bzJcLwan+(m}Ke%Vdaa(lHqzh$f3qfG(nGpzgz@e`_AGD|lp)AL{2I!?P3b2)cly zm227=zMvx$nNKxv_cSLe5jcGmD3{TVI*BJ^&8gKO4V|UM%s;abgTw=43=`oOoCR{d zhq;k`L#v1WpwqL&nr@Pu?B~4kQE58qRY%hGE7 z$l{>QU}kY830FxhMI6gPv* zRl7ER^nJ_p^E(L zZ8?om1eBYN)ToFS~col(z?l6`s$?mi&XAxU8iP-cX&=;Cl7k;01J@49R-25!+SLvE5c?|5!gW1jBo)$&R5rI7E-cOlxdG7(d!0^dj4v#r$jB`W(8CR{=P82V$Vd3{ zbh*k5jX}EWTHq`7X^`DWEO#E;lCC$G;bm9BgEH`2nrPbcEKio}lr?+P?1|e{wO{Mg z14u8%ipSEWlbT>;?IEE&)g&qQeg>y%HLWk=hUWgA7j-1?xQ)OKXqxp)Gjfa-5yuZ- zX{Q2#bksqNX3ddj|C`wj)$=(&fLy<1w78F^Rpu%c@yAuDY_&7v-IrX7VsUdX=@oQ| zo<|WCVmbi}_y*4A$ckBlCf}xh-W)y>*P8PsDGglPcss@nF>KOcT38%7Pixt-pOXVJ zHO3UPweF0A>QdT@;TxO>YGBg8yaKVQ7Tl54j)ke`$Bc<JB&@stDKL#b&tb%xq*Wepb&k%qGIz4^79q&m^WQEwZrxO=pEa zX3SJ~&JxlXY~jL~YTcq_$&wXwI~|RDj{6;1y-kM*03jfuNS1eLJX@dmwfZ4@r2Z6q z78B|@k{o8lQS7kSgGAz+C{@O7Y!KzNJFbGu#Kh>Q+IX9(PC_FnlT%bMpCgqASsy`3 zkp~5WUv10U3w<<&imzsEPFR-wjZuQ!&j&zwqZl2UWA0;>VSM$cQ2ZjTWA+%UP3gMQ z9}UdVIXlVYs_yXi8Pcw2!Q9zhA+6uoICo_;G*X2~!x5E|@69G~ukSDc3kDGa73>o( zmTS!erVJ8MgP%{p)@T_ARp2`jDRQffw)(O{sFkShlJm7Gb)4^1@qgE*>%&vwGK4uX ziI51>EF#~IQ4ErP#M-f;HSNz345VP?x82*}71(T+^fLp!sw^*%yauDSh~ILSL1e#P zLGqRsQrf#F&Fl$Ig|zzWJHzVtpPUySh-_jV9Ad&L%@%ZF3ZE-=@N@*s@~5TvBEQ+B zNYZvm7OHgZsJ-0;^65g`h(b#jw}@UgLsq~2VkE)Oot><%MrxagG$O{};9b*z9y~%q z6o3mvcS&~C#coMysK8{q`=)Lni6wWfY`b|;7E#=!7{7!P)b%wA8^+IjCq;g{!LarUBo$7&)0;zp!LA`LkW6YPur3J|@kVo@{@eyy6OXG`Q!hakazt zl@Ef_V-9X9X3}klg(jLa9tt1))fZ-0%Fvhjx^qzi=jsIP4R zPw}V8?4OTT?W{=EvwAr&t4$41t=#Y>EzE0h@&QLzE+%?Nfg62*WHl+7I(KC+tDl!h z^Cn|-^nIaIcvzc74MLcyw--~(0vjqGT^<)U(y8dE%MF5N6ASBIC-un4_}m~sD)xh% zZkj79OxUj4^m{A?$VaECR7qkZ(!=xt_*%sZCQ+YFR zkZ)NEVSHheREqATxWsGm^`@Bzjxw-evk5D)sX~-N$iZwu<|xz6C#)bOx73 z8V%IOk*W6vCJD^Mh#yhu8pt=!)1PL0$vo4TU^&U_`tKit4D_3X(o{m_hSv zALCsLMs1yy&u5giMV2b}eRFiB@tr>w7n-3gjO~{M7ClX8=F9Z4DB$4QC9tW z;5(75v)KD*2l!GUf2UgAvZ1yKK*yZPvKy8MjXxPWv^lZOWn>a4?a>#E>0)=1y2o~yXqEH&QHr6JRf8j4BKR_N+Y{Vpc7wcw zf&MGG{YrRk$&U!$OJne5gln1q_!*m6giBD ze1&Tvb2LFA4Pf}}k4e|QKL3a_bKs4svWiwnN&Y$=ogPX#nFj@Kp@~yq1HzZ_k?Jzq zenIqZ^PhirY;7xYos`LIshA+2lo&>zlQU}a^{4WaSx6I3ZujKOzt=$(;#fPaq`srk z^({(-1o@OshcbVp=2QpJVJM|n)BXT51$ge($h&;LbD&a=SFrab$AP06Om$94Yvi!P z1}M3eN8Tw@SO3z3`MnzYDQCx)hb1vQUlq2UFhulZ8j-LxZ9tO0 zw*?t_R2PL#V&=a+i4Q>jw^~FrJtL?Erly&^ubdutI?SUS~;iqI7b{n!HV?T z4LGrw(%2bji{1N z)vL%!Hdn#?ht5fdJrXJLTTkZ?r8oc}cAj@AYHcWq&eZa8_gK0skup5V;4nQSek)9K zNMux-y<_fD)mja6944 zYN)i%M?%Uy!y{agE2>(r_z#_3NuGhB8jG_F4{aEl8_lRLo*|2qW zTkCk1OoVWhKm04bDXKOqwJlWEVyO)*Ur?&d-xZ6XHtG(UWcOQDhJuv4wnOnDwjz=o z`5tQo+Zr7+qS!h_w`hgPm8woA>0KovV!R(!+;BHCf;hu>AAw8I*(k(9U# z5z}04#Y7N>7$R33gVHuFTQno3TKlZyKO#Y74Co}7yM{>IRmEW^q;tv+&xDIUADL=n zqe<0;SvhyZqhOZHPc-9y+XOUo#73ctb{k@LWr$F^*pOCGn};h(-W}b!34}sQEiL8v zM>xfBiQqi!*A9nMTj#Q;uozpBqo&J1glAiaSex3C zLFL|7Z?v_YOA;h!@$_@RUwG(hT9GbdlLWFFo;+2wx74~W%|TWc_#Xiy{KX=0t3f)m z$GAfL1k_?NQ#~2Yui_6h2r-vlwENMa6vD3 z8ojE5V=v(7%2lL}?O}6%rf?NQP?%kF(_pfPS*^ zR@T!$5WBASmKterEfhL1XDEtDP4Up2q!4A)VT@5h(^SR(9w^Zwbx2z#6LCws$LvzP z5{oVB{1`V{5BtCt=T;h}hGT*eYdX+gLm+GVTb)6CW-FiRXIlYr3G6l$WY;rt(21Y0 z$!Axg2W{xtbbqG*0h{a&fUFE<4{mR)c%sde^d5N;+fIvDF{*rcw9$QC-DbC*^fMW? zF?*k6=H&UG-Gp+JwT({L5TuIo)=XEKT(mJ7ivtXPU$m(MT|_@sa^4BMT;ry7v8Z#1 zn@RGmId7eJvoy_OuZ_Kc!r{uc<+iHE{Ff+D?cy)qq(H?#i4K&PCdVf=Up5ikw#Xvk z%GQ*FfUN!o@uJ-Vcu>oJGPAq~sRAJuaoY&m`l719gG@|)VidZe{84k_tH{oHuVXi! z6y!|ZYKa?yHLK5D<<2RpAjp|^4DCQPh`~i5H?$KXSyW|?D2n+ZrbSU$YQo9x&(2nj zARbGxNq?51kg8N{cQ3uccE!;CB}JUAx~?TE13$H)rYIX_u!0CiGusF%)8>ICjMlp7 z&MT1((PM+G+;EdM=uIyo&Z}rCneH^xz)b8~PBeE6cuGP!P6Ey{Q=etRlRy=t1d`2} zWT0@Y>ICPWXDjzO0^M*_<-xKHmH$H47HU_vw&GF}K!PJbRe+97GtWozGP*zNLzMx;LiDqa5c;e`d_Mb=ZQYYX#3r(GWzIS_xW& z8g-*7_(r&b(y}Wf5xS1V__j#b*g)p&SP}D93Y~{;x#BsT)Mv-RK!)NDg+M#Y(_gBv zNRb3rNhanNP}BxeWo_|U9$CNKYpW=RvkwXK-{FeE(>W$RX`p!^#&gg$2gk^#TU=A< zAR06f%nTc(j;GTAMY3%b=xVk5Y*ao_?o|Q!k&;?#h+_;tTG1zVYKylPgbt5QfDFZmC}XeOP0sA7bp~Q;L5QRhwgaFnB9E#8xCI1JQ1PNCAmhT$(Ks&Qt+qEohZGy8)fi=-*u^ z%+8c#ZPH}Z=$D7RVl>AnDV``ubeuLvM%L|;V4V~@0j}4IueBG2Y$cn8 zB<}gnG;}m~4yLy$iei^`jBx}>_%F@vKmv3Klpwlc({+Z2cO1Y_YSZUzF%cn&L@X!9 zlHCbZ^@+;!zwJy7V0ysUQu56THNR=QF@T;<-t-twm1OM#PU=F(3>0Bjax)<(@tOnZzi>=GBpN>Dzf5$)gXwsa+p?E4L%cfx?Gj=?^txF;f& z%J9fmihaSpa~^8Y)z_|NBPsKU%}$0#TPy5$cT|NzA};p@LnN=3npK?X0q@b+?a-B| zO+Ff=(-e(~mZ@Kbo@nc90G$$`p3fP-V`>=?RC1>8VFUveQ?`t>wH+8Y$amtACBdUJ zdj#!&&cYIG?G|%Z(K3n-TQ0F7=3D>|`d6LMaknUXg76s1+NchUocLN35Rn22UFD+Q zCX`&%y?ci%x#}aFMj@p{wZ&uCN0jyW>>o}wv*)auoV}k%1(15Ss1m)SLuB~;-g zt)^(>g4kUhqtPG+b088Q*mfc)gsnJfO409a%;|DIYWeRBrioHaXS1pk=Lxs?9Q{G` zP(|xF!*-yaDjb7f9n$gxJV`v3T`{RPPcAhq?Gtph%iRSF{H2ZdS;;+9%vh)Rl`TkP zL5SIQ;uNDW#IS}E;tuGstB0?g_oTltx4l6n87LVl&~>BIO(V36XgxXB0LfIBv-7&>r9W*{E8C1JZ9=d(Km!>HCAs@JVAMgf=MIWchOAp|mx_pdfInw6 z{TKL~5g_IyGqi53(3!SMv8m;8BP0VUu{j?Gz*cluQShcf8CS0uOpqI`V+`(MBE*)i zHnt{qKtvpKzVLUPF80Sg$~79y10oxU#|Mc9iUm>=PBqY0v`vTfPERswW{@I)flL?n z5TQ!@uGOa0@}nx}jvyv*HEAX81HKe3n__2O{+ljCZDVD^do!G|p40b^y)U~hB}%$e zR;py0ywCWYU!a9Tn@?HJ7S5xpQWr$N|qOxHQ40 zVMYn6IQuY5>#mdm+6Yt^wu-85Pa&SuqKx~h-#x0T2nHyhc%i4N1Tkz(<#)(K44njavqn~0<5Errlm`v!zo$UMzi|C;vHZ>u%ojLe-H{t+p|T4 z5Os8-H8S)kn!7$JozSY_FW2n!9$u-+^e7}trtEZ+5HEw)<5M~pnlw)*Wf#LmJyq&1 zg(*vAj}SI8X4oBYda@2Dluo(Wy`@%r+#LcPuVmNSf-sX5X(FEFHm-p4?g4g$l;m>C zhkV$y7wi&-vZp9&B)d1{1MMgR><@CtLKVMQl~Yf433HtM-`P zrcJflK-3Eci^ek1NWnzi8_YlXEP{BPm}A}4^5z;MTJpz}spQQQ6UESW)*`EXuWi^{ zb%e)X>g0!;oyw5K&$cg;z1!AysZ~?9L6}u#?p#WMBGu*rZ1T3PN(Mx2boTk#mMZJ- z6O-vT6NHWxc4x*=zsq`z%6Epyuni}h*y*svm%7I!VQ?+hbAB{?f%w_x z{MfGuqSKujD}i{u*D^-chdyRFHRSMP1D&L^aNBTpXgJX%H+pwhqCEb`Vyu8vNpMjF zN!TS1LljDni(+UGLeagA9zbo}P`(`%WRzUX|L{Z)ZlqCt#68q%hvwQAhJl`4ae~wD za@_gxC$)pxpD zTk(kjSrjI~a1J8DKy7*hy%CQ#wL+2w@aT0qCYo1(GagFNzJYiP2~J5lflgWR-`iB- zd7KA1nWkOhbbDzIlPWrs9!qx>ZuXRYV1K!!j4s<9`@4n!*wt7|DQ{w`;-UO+i`p+S zWLD|i(oPXu8VFxIS)sRK);YcHG*Up3KA{w1bV5NPO4MCeMP9B_YzL=mdm79bUo`fx zNCu0Yo-`JJwWzfnSMVT3RpL$b#1j&={x7tD3s6M@uthl_=U1{d-#>5KPz zW+yt%u*2B#GH4h&K*a2d9zs!*J6mn%b{z#>c^6BkQJstnY@)myyG79aSWiE6dL-@f zB!0TJQ`N!wFh_HsysJvbwzVBkm%%85n4uhp<&_6V+=w7moU+L=nx({os<23hLbt3$ zloK&~P?}u}o!S73s$#d$1nNob5jkdsE7+)Z78$#w!?p9%oH-|wM^Gkw1nO{YuXFKA z4YWv37Rf}s=rl@O+Fp{B8>Lgf)h5q50?nYbS2^XS!F?P3(6JuK_H$tS-{U+uRElVPc zEWHg;wlaN55_@4IuVgx$>7SC)Zy8+_IyQ?ZFK=CRy)Avt`U>GH3cU?PbV|Nom0gBt_7r(FiW70WH?=F{n2dOvo-uz1@nm>doATzT2qGxE z&Hxw+)qg2<1Tr4(E2E*yZu*>K)sznSlLJzE+EFTTt3Vqk6nHqusU@G@LmTxNB2!NK z7L;;lpdgh90jH`g1vu16km4t5)gm)M(fP|~sSu%bNzusXBeba-UCYU9!_&~Xp;|d1 zN&s6qgZsW8BJijy+#B{lZ1GT;1f(>xRj67wimiIsTR9L9qr;k|!&T7+Xe)Htu^~95 zq}5mUMUuBeRToUTGtr%sZds|9FPxDCrYeP zv`f#aLJI!;rZB%9EFPwl{nSTpm>;MEQD}LoHD|7GID{o)a}ZHWB13}McRrp#Og{rz zaZe^Aii@(-HqeM=tJROLwC~7`9KND0i#1v(Vuf2;i=vgrEPS^yPqfV08WAlSvhGl~ zfm$_kr9@Cwr{5Vdm{VwLV@s+`ZYxVal8HY%luKJu&pl}{b9&HaF|B+CEvNtP0cmrB zr!Tk4x1Na2`Hr)rgF^A35og%!UWYhVq}qWJL^J<8+5WHyR27CDf^Kq29Z`RjVXNVr zDvytjt<%h39bjz{DSXrDEQDedN)B4aE{A2O2Sk-9WIAy(M3!96M5!tsx|z_UJQ_f1 z$!cupIP~2coP^l%KumMuRWt|uC7%;zKv8FKTAJR-q2hy(3`cM1v`f}eP{G4Tdcp(>}rVOgrn_{(%xMDw@s!hCJIygH-QQsIq#m4^Y##^ zquk+u4OOOEVS7df{KZ@mu~c_>vem#agREzNwbnyq3tO6o&Q2r<5B!`QwKWg6B-nOZ zd`6_~npPW#2vA40Rq#uvheUkqJ0ERqK{4Sxn}|oZJZ$r2>pzH;L?I{fRu5aTwhVnh z)=m2iq4Yr838)Rfd^RP%B`?A3!R$mc(886%^%#yX@is9`aIXodB+w5IvNhc&np8 znsfHC4}dL#I?UQ$?!1ySEnD=%qhjlSGE^z6T=9ghiUx-n$125G5ks_5XF>8Ym$L~e z;4hS%AmS3o8<49JbaNn5Jiny_7&bA7KPTc=8<}L0QbT-XcJv66$6GQ~#YE6KzaqAV zAgKA|S`=4vmb8^@<4g=mja5-atV)lC&1CU(#RTl01?8h^8+AJnjN0S@9fFkn=~1G* zGPaDL-Z*a{UV0PyAoTJx9PC;Kvr7k`v|3L z1#IX+Kdr-V<~^dDsOGe(FBlJV6sQ=A3l!sF90)pM@|_03d#Cu|mUf)lu43BROSg!&P%gP>lLdV%2!0 z!;*tARp^PULN8Y3z+Q=apz46cPYijlFjUl;Hp&JD^lZ%Z7+*@=;0|X=rvX*4D{QqE`e!6$YFygrxo1OxgajL&!|9}nFOH4tN1L(*jfr%Qs!Y~ z-~r@0CBfWz2xpEIO&QyhVCc564Km@3y3TxTr_{73t`0KcOflkp8hy}{=!En?8;T0gHjS!? zM+7&}uVnG;n$EWl5lNK;Y3QbuoLScVoN8eLIs zq<_*Vn4=s)wi?y#1p!-Wa1n{pSoWgb(BGa;xzTHmJn@cGn76}Lsm##t#+52la3lJm zBavl}|wzAT9HuDj!^@?9r76G1(#A>*PH8lv~u$akhuO9CAO z2qg@$&i|s&4AglLYOf>GsY*sJwI@4vWh7+UNtsr8K}@8LR-!@KwXPMBt1VIDp*Y1! z4ePcEY+?(Ax~nkPxK9y&F~n$V8kj7Gms~Q#W-^MAR&7yHXghXg+8ODI+e!YD4BI}Sz^sFxg4OAK z*PR{Afd2SaRJDjj^{R`ob)yKu_@+VP*q|rpIBy2{Qw!w0d5CAZaYRR@fzYMLZIJ`! zk6^Y)!7m;sIb$L^p+_y5B&s^Q@mc<>P(080Nf%}7|9b$Jxb)ah0E;-;ni(LaxZW0nh@c8c#1>cP zP#YT*%^6oUT3T|NoLEKAK#D9VoEb^W4B>IPY$byM=;(LH}J#a0d|((^Q9{Se{mwhlI#&9*+CNJ z9T_XgQ0sJ3r!ZSN>e@b_Z-Y6v!FtLTz>th+t8E`J#Hg6tgTJ$z+72S8%EaQla@rQD zwiTrtQe}EJq%gvh{TCpfp4294ol(7cVvB8*M3AkLr6QtpfzKvsNO}??yBwr}sp^!F z%ikR+;&9pudLn|R^jJ)ak~5u0QZCanOtC9}tPMn*kqrY|jl%Y+DccxI06?))nxD&R zAAVvpg$#f{3?OvSWk4DW;G`n1$_>{7C~-r1{Y? zma1r#k~Y6mV77>W*1iBm*2%TIKpK1^R&yM>=*d6?E762(Ui7eyV0HwVXhSH<@dqUV z#c0+pYu7T+b`(OVU1?gkL&KkGQ+9_p(hM7vQsrcSm4amClJmHb@dGEBoR6?VRiYza zb&!ZXDO%bt1rSM)JAX>X3V|dn70x{YBg|$O(q8e~E<$AFZKvE(pOj*FLJvARf@qV% z)-*L?Q^rox_C6w&Ms;dQrN>MpiD5fWG9WtBA_?*zm?QiMh9XXns1QSEBU_9CXE2oG zMMK?j4hHmRsUb>G%z)XPiKxc<5+?_5jCRe8C+b)ma%BM`XzqqLv#T7T0I4L|!bFt& z8pZ#GiC%9=#Cn4?qA1-Lu~0;GCg?Jtmn~4ctU6l&Pv393qOA0jfgJsTq95=!Ss+!G z5wj_Mq_W7h)nt@fO4oxx1Xt||X^V&MvY^uwwIDnlL#K|vX>3a(IBeNXCKaUX(my&SIiub%4?a_+TC)X+#8A}qlodaNJ{?HP z3e{bOR+M0C=Ci+=@04&Aui`dDwaA^57Q}}DqWC>5K~AIDt+1&d`VBB5Yjz+H6q6(>0M|rv(g=E-5!4PWGhtURIJIn2oQ7aLH zH837>((jPlJkr_o1Y|_fR}l=vW4qGRMj*B&2w5JBC0rSseuoH3sD%g4sec+D$2ugd zCbaqZ!!~^$ixG(%@+1v}WIy#V(zC000)?vSq?t~c=Vh%R!c?oaU|T+25K_G80YWqe z%D7c{qx~AHflur_l4`BiyF*M!4Mxq8YnxYoQS;JQH%G5;#uM>xoo}A}bn~g-Q`$UV z+9`|v6UW#JaWX~ca)#=#jk!b18O?pDi;I6k+Nf=pX?5}+M);+}dbF3FQFS_}^)@J+ zZ9mAm8ML2TCTQ7; zl3bldye|mYbO#r;mXILBH+jMQK_Rxa(IbWJ;2|1eOXsZK!4_7Aajx1rsj$T#S!{2< zL5aznC{ra9KEhMUDI+hh+YO5rU>sStG(U8d>on0eDvgbA@X!hkIah(W2T_hWPj`vm zjZ#(Uevqyn?rd3wnrXJuUx9h}zsAYMOP^B-SIm*1sNq)C7X`4zJ(MS(B7jr9gr1Ja zl=w`nlII>0_z5^Y=?rDTSizj4nj6ApJ57V4!Q(0rg=uC$9%5><`#^Z)e9IYcX&M=j zfuW*J!zm3e#6zI2bDm@&@#FAT8g5d=`~JjLl6taL9^H1W0P$E?zSRaNj!B_Ysz3yB zIEIQjb3~6uh%<}?wlJv5DV&n(3&y&~Pcnf9q3@t>x+m33zLbeq;xaP~#hjB43e%C* zLBY6bn+6b6hqx0VMg*v_PU)XCp#zk(0*JwR>P2tQgkc{uyBf>e+01JNqplMuT$bd& z$pU0N4pBwB6=EbHqpGCBWw!1@1DHxJ7)_cNy|Vd|FK=G=Rn4nk-@M|B9bJ8eR-Cca zV%}VMvibOLH;?|i=G?z(e(%?t3r`j0U4#3OOYXG}u))}8CNItILAmPw3J*!0W%2Mt zQ*p+{m9|c2YfgWi&)`EuuyX$`+n5V8dpeyK-p`6ALWfz+@h*U$ zI(5D&Y?5MK4ur?xFU4&6BWGOfn9;1^=o2xCjQrv7$lz`VE}1|gJ)@#`b(C9Al(ljy z(MEz8q%9gwCME!g-u5Ao(VXJjqZ&{}f++WF6c44XJ+Hxt6o+LIG+l?W1$Q7CKs2x= zb$ul(HpDi1x?TeCM8E%+V!~Ubpeu^FIjH42m(x0Iu*XV)cL(e%XVQC46bMv=s~82C z=&T@-K$R_k4pHLP(*Yt?SRs=tX`ugeME68U>P+jT-^jzmsUJ4#!T|8Cge&WWCy1U7 zYJx>*BW`b*PcN8A1hP(vWC!6@{DE!}s-T0;NxbTGlA!EpR}%b82j#Q%M5Hh|6K(3$ zXs|63h{8V506%o)ts~5+jlQ50k3~vsL$v5h4~}Vsm;t1FsRhv-DFCrM45b#5Yx9y0 z9U$!mkZY8ea&fDO2zh$GbATdJh%>wDtOU^`DRTmC+!pPfeyu9?r({})rycOp=Gm3_ z&8O&PpVh-lN33|9tvIyVh8Pe-413>Lew(Zex8mW1Ap;5#30f9T0@WM8uDRu#npfZ0 ztY6zq)|&CO#44BTq#i1&jUEMW3qdCQpJ!vHt6T! zfuG$kyTAiTW2om6P(#cRIzzK7YAhO1VmiG9q}CY#g|sSKPiG?DW^83F>fGd51z^)b zk_;Bx;U1X9s!fefO3}#{g(Mr0c$RO-kR_pz0GklmQiufqwQXC9=@pNG6c%^%7z}AU z{PLFCo@1JwwD%=I=+du3+ZAnxM}IPj4P;V_`xp|J;|)Hr?FImB7?lGSH<$Q{G!Vk9 z8OSo%7C}6bX%{=TAZin? zI$6@noOdxrGJtsuj$s$dHkbkn=7`pega|@P4~*KkU_eA1B2A+{b^~!*Ns{%@!2@Jpu#mJ2x;_7@%t-N!~iT z&@(WEs6E~r?fKzwN;1?Awj8;5!)EH(gIb!56}3GFtc!@Ry22DnSm`c+D5hOd*b8KG z?5A}=cDJ|J{{^&3VcRW$tq#Wc>4PpxK-(*YAKv9(B4W9kO;NoMiX{t!3>VM|cCyi) z?QQ>Y^U04k=l&1%Bi=BP`ik3|TfVV**%vlPu2Z1ms%Gnx%dL+uXCGhAKijJ@;fF1@K=yBOAhK&IKdFl*@7^aqFd4F@4L+wmkPy>LDVG$=6n>9 zB7%n2DdmbkzAS~Fc10E2r4vf>5jjbi4Hpp6);ehy+vth@xCGQFUpZ#zOM^B-X>xlQ z%c7%54*Nh>Y>0F2BucCSSy-Y0APbRgG7@Y(3=dn?QqeMit`^8ccOA8nML*kAA)5*l z3Lqd(2&admlwxEhfTG&Uba!Toj7O1+;)pp&~-xi<8vtTL4Vt3Mxpm|anIFaZo8!qtN@6RlMj-Oa&=Ts zD#T=B1rzB65dCaAZQ30f*ljbZ4KaHxdO8L&{9%)3!Y#VzKtwF0%KcKCh$;|0>OD)r z5Toi_Cmn!e*Y~+W5>M%U6X>YgGw=K+Xg!; z14@lz(x3W~=BZCN@Bd$#<-GZdKi<6gTbfsVQFHV~&5*07KU`k;t;NQNm*+pWJpXue z;WNvP$C?=tYYR2H)AQPI8ea3t!H8)V(=Q0)W_itEdP6gP4HpT`Q;#*j{qLK9^$(k0 z`#>{eT4w8kn&TFU=!(>KEsg(bm;1VFj`&)^w1S4KzuM6zx=Jgo47G@0dWPBvl2^tI zojGy#wqg$5+w4dctuSMhy3|GqbWluq*B}Bh`zsox**dc;yAh_^!~l<^h`^a7qar9D zPE@hwNC=VI@WdQCHysjShD9!!jsJAA&YSsVqRmm8uu2kxrzQld#Hv1!A{Dik?GUdZ z#sT%7I{}~Kd5~IBOhp79%#jM+=-R73absgE;++q&s8)%tt^2LZ1KSr;MKZf8x_uN4 zV8xT}ie^`JK)HG^cO9Ivv$PKxX1O@c)QXHX02~&N1wrX*uwA=amnOJ~G#a{2rWl4m z*6kI$RwgYA^$=Z@9g9%1LXC-t?h%?SmNdJ&kv29U&h!hlAZ=0ixbJ;r-Q56dL zzSIuTEg84rAq5%$IO%s)U|0K^P|yNO2U^4+DI>v3)YZv1`dq+;nfRkHerjumv?${W zgaM+8^DLn!(m`<98P4&n}<(&E;o) zclpHcHRGdn_u}b4Y~~xyXgbjC!Qo)GF?!uM4`2N1!7#nQkmC7rIo}uzUOYJR=4Sm% zbV2clztueSPnv&ufAjPcG95qDP4P`zIk&2!NWXN%&TY0X36Tm#JXXkd_n=oPlva-j zzV<0&xt|&6*~_#*U9IGO*R zljfeGKqj2jlZdr|nFLg83V%9<0e+Go!VorUV}%0UsAJn*A^qk7R7#Ii1k+dBU_*t| zMOe`QbWT}%q8V*wKs>kY#T-Fbb$OtD)FtWDe>q?V?P!{8|HBW^07Xr(iS!AXt~tnR zzncO=gaN^HHOCq?66|e(>{`6b?}XXn30t%=2w@eo+d}HAO=CeEAO$U145T;@kvxLo ziYHX%b+J+y=T&EppwrnYZIM`o6rfgZw?dh7EkjgA58#M*O->QJ3>au62wjU{V~=n8 zlWD58&K&Y2<~nG`ZVnmj0yc1}HhDz6o4UOtuUs2uHP-oL2&g&pgepOQnsPRi-P{+Y z2KQoDhk$wK%(3zuO9mucDqv+B#2dDy*d7By%NCJjs1`%hy!I=bkzpEuD z5hp1u)OH-W38YeNiKv7K$fpR{mQ1S${s5^U{OnfcLo{=oWMGltKsf}m2*v>^Y)__w z-B7;uM?>7V=_n#qy2Tl4^LvIBXWL7Fd`?C|+Q=SctA;p!Q`GsIgU~nqMQ{0&C&hG% zY+-|vDiTP0Roiwir|C3V-g-r1j+U_Htbvf_ird{qD<*I(+Q_pNr;Y0P_sJr3cu1gl zsO&Z=0E0}M0_2`~IJr=TZ|M*<$_yYyAStI1!?{8<_RtaVp9F8hm3(j=6e(|g3#cKf zEiT5odZKlSZ5jx4b&~!e-!>{$MXCiNX(>p*m98@-A{Z?VOa_Xp$dgg6&!^j3y#uY3 zlvsH(Xu)~}(==_(9A!Ga_WC?RvTGi-#dT2h*ZDAapQ~mFm2qW}719r_I0^$Y8F(Gy zl18bA=$1crla4~8T=ni_D^3e`jy%0pXM_*xVt2B1HM-;|syK%GRCcyMn-W+rgAJceb4oDMbI# zwucxjq9xSb|+T@Viiw^4t4^fWva5bmf)J!WCkqq9N$q?gR^^&(n#U#6}LA z(y<|x(-tSNPpX)QZ6-5aiI3mCl6!u)&RRMpL&fl$MSsTz0_> zz2nbuh3OrDA?JeR!(TeEJ^Vn3a-e0C6{ke3oK_@mm*@lzJb+zLqNbacqYpC7>N<}E z>Gz>nnxAur8KP4nyUr;9s*0=hr@J&64KR{D$?>>cj{fS`yrNiz zPyN{QCg86w!0m)MTRP~n9UawQ`5n#IeP{C(-`c$9re^J^t6Rv#P&1+Vr)Dqu!jFG) z{OGUz{^Ni6srl@QkN<}c{MtW!-zR_dw`Na1HGhiqpd!zK#ewb0iJEwkW{RQ@&~%+UO#mgTf%j)^kk5pzjy>9%G6+6Nyp944@RL;uKF` z!I@{p8A5RYDNXQOS%`F?j+3sq+gtY`f8`|S`t~U?MG_ujnznPT8V`$XD@_>&HN={6 z1|B)DI#F_9Q_Nw**)4Gh6WYROT*jvClRYE z4Caa2qTia)7XNV@ikjB;Mwka4(#3zb2#|r2@erB>Te?*`wOyUs!rK1THH14VjUtZ; z#MLeC8B$?0P?Gj`KMh6@9*MR*1wL?c3XbVGMnk6E^+7jFrkU<`TB^He9$Ni&i3dF# zaixP)C3@RMWa4eyC_2r#Vq?G0_1;K|YOKcz9s-p-sKjYoA(~4_pQM+%mXc?nLJyH` zd{Eb=_o#|aTIU9$IpT~Hjd1q>oHA-oYRTwMonJE{w0ls3?yjgpIsQPA+H|koh}#iK zKah#5l5(=+3($W zW{1)_xFm}<$ws~5vrUpIqrdu=&sR>3$9c#5I*+NwioPA*|xjK|J?xKTA+rs%_J#rqPn-;h2c!T zO8_2r3=mJ4Bvd-b%GzWTO55o`YJDn<0(fWw$3U1nqj3)+B=|+NU>E}Fv|jn+a!hD2 zIoCOHk>&DL63XcB&l)+y3X3}|LlU;MO(TWHYnLm0RK^vb9dQg-ip(9(}C*47OHgy9v$8k+TBW0 zN7;~y7ECh(@?fRRb^-idQs0tsFLnJOwJn2+>DV;H3eiK6wB>LmJ(lWh#WW-$oEn>U z*)&#ij$ZS=08;Lg52_eNfLH|&zNIa6T9m+3`lq#&Y+I6kMxEStF;elFBn?R(t8tr* zZmD?)b=uCdzeMCA+^&_l0oS~yN>&=y@QpnPTM)!aHPcF9MjhMXaIidabFl%^#p-U`6O+gm z=%FjgA+%do)Hd^lH#Xn-SDG)nMMqSy*jjw@LyO1%?URrH=I{OL+28v&kAC{MKC!s4 z@zO6`d*!Rf*T1N_=IG$ab=OT^{L;~N*9^uJQZpP6mh;Vx<5F70}U)t<-kRFl6s`fL^+?<>`7!6z>C|jrAw(hpvi@>{>Y>gU+1mV@3PSyG?tm z6%&7JG&F5Hr3O3@w(U5J1gX;Qb}Q1f3=XMH>yl#52YCR4tos*B40Hxo5_E_$C=E8D zqDi5Y>Y$$w%#TrqF|r&0@jn)In6(B_RowHb1O}b*NVy%~IuD8wYmO4JxsB+@e+_`v zF39Puvzw-+R%|T~#I`>Q%JW^zDuw6_TLPr+?Z^=o(X3kMW!FtpueN(3KG4i`0?mx` zoze6F#SrD52eZ!r#d?_0Nro*vYO=Sb)!HS6xma&^ZWvN5!y}ghaod79`@k0P7v*>x zs3EOsKvx&jKEZVgW3+U8TZ?t{(6q!bSxl2vDh_8GmjWm$My;Z3=e%2o&PWBYL09CU zlUrG=V+AR!%O&>`wQ(s(yHsm_z($;{{*F@_GXr_ma>|N%;#h2v2{c-~@GnFuvF3<~ z**6a;vV{R}@tLlphzKc22a;mWYFa@%BhGehp>%O{KKhn$|ckOWTsk5_B{EJV1^xyr)zxuUDf9a9W{N~55dC~BT zZ$9!xH?F;vTEBMu($72kg|B(Z>tA=>%dcBo zeEO+BeEgHY^VsHv`88kgiX+!emQQ?=)(+O!2F$buYlCZUXx6ULPmDiKFms#L5Vp<) zct{o7oa{Q!!vG=uKuTkwYv;MK(UTg62wO>6vgma0<O&9(xOKQ~#E>fF;3(`QUxJEwW`_b`7 zokI?b7VkQcjNC90NR5jfRMj$&;Lqtp_esXmj}J)EUXJ0e^A%>?OQbXe>zlsMgC|l5 zlaUrl#>l)1p2+5cRHspksJ4A`8Y1+Jy2UEO4;vcO8Sp>}_&Y?W_f!y9BZc!D_8-JI zu_fa?K`#8*f=k0JoM=eQZqG`B>MZ4+;iPN_1#Hn4dQ5({kj~Z_8cUPA=?ho5C{HWG zmR$)TLe@mevU0CABR#p;PaRx6KneJme*7wc+7EbRZsEFZwN;ygW1y z`54{Y@~d^*0O?ArVw0BocucV*BG%WuQlml12Ko@IJ&x^IBuN*4fCa*XINb{8Eq}E6 z#y{J<>@~~HM;D*|V6z-I^V#gvAN=^E4}JK9kNoa~kIbKZ@)fUL|B^QydChC5*BzM* zrY{-2?3JUJy=-{h(ZO^wTwfndr}}XJ##76Ur<&R3a=92Bxn_Fwy6d)nZ?bsu$;TI; z`rXeoYirkk!ONz@t>)>^G=uTrnrn1#aQ%j6{iRw=|KWeroPVnA2)8*+D7(tOgZ79J zkt!wcJUAkoma5fhFE!p_X(_n=70u`0;8#q2p&q17rp<*X)FYh{tn>gtmQ$gkh|h@{_dYABe|i@|(CHQ(w{1TM zRQinJ?sX>Uy9D4zmh&n75TIF6#GU}e!w}6Bb2h;Tj&^vm@A`Hvr#~FMiB+8{WaBMv zW95Q>oSOqlo|z3*GHSbIilB?J!bXjanbt0dARUWQ-Fs|}CZ)q+Pf8KraFR^Nrb~M_ zM{LO?9}^+k0rtz(e&!xNZA4Ghg0wWTDh)wU@Yv4zu?tQCne1Qbv8wc2jFP9%i9JUq z&5^0X6&Z@9_l)9g9Hu#TtSgG*e#jot)`mL7Zd#}QcYcM|F{sJJRqbV3WSuD2;9<_4 zpZ;WD6ECCOS1oiGoEIF@sLhQ^l;Dp#-^m^qqooNsIrQ|XZ(%kUkc1rr@vShMo`V-+ zj-4d&q}U#qXbBx{m>^NiI8SZ<0=d-cmmZr-COpW}I*#A?A8pCUpNyO$o5Q442pe4| zhi#hn?ts`#Clgsv&Z2U&_>YFj=20lk)QbZ8;i^q>zI26)l5$e;CNiA7$r^hV{8zR6 zLc1)rHYo;ggH(kxc;yYvpZYJG*WS8Zd}i_31M}bhx#3H1Z?1jC$3Oi3Uw_~i|KQ*J z)@U?;)$7-9xcSISt{*L)8VrxTbo`1}46k2nMw7vjYiG;JXFj$0^rxR(&S%%YXng&3 z&EUd$J)L~TtGH0z{LsIgJ@yA5`N-^%hc{+x*WUEjn_qk5wE5VFhu6Gh^y)7izW57= z*L?NhMSrY$;?vE~y}Nn;|3?juukn9}tEF8ng?4G%Hc*`?LXW5I7I5@M&5d94|7Y(% zpfx?t`%bXFn{Ov{ZlDnw83YKDAec!pE7G(gWm%NQN|r|J@vP@~ckP{H&n`Xo*t>hq zdUR%etXX-jv8b`xu0YcJxW@Yj@ z&#kZOsV7%@-}ei*Ty%{NjB|GD^N<`fODD^bgXP(W$|JXz`@T?49u}vP-c^nCOCR6) z^I}dDZDjaNfdAm*N222)XwdvVJ?rT^H;7ZIjbw@kBX%pzOb|-2bI= z=V!vJKjab}1(Y}gFr}a;QXIJE+sa%2R%08NoByEP`R8f^GoI_v6-A#w)|1&uK@*Uj zo@5aOnV>^TharVkp*IPp>cRloICR+?5545TKvog0OzSv9X)kI;Wv3Xm4)XxS;yQh~ z%Jbb1T{x391^{+4LBB|rTg8M3l zjAo=5i`*;di#3d+hZ3DhqcZPJ69iz;w&l|THh@CI8t3e@65@*VCyLYu&ID+pL`S~6 zs7v|s+FN3hZ9`dwTiUUxyKOiO+7^}5+7!T_6hXI%=mF83=>$X~R>T9ZOA$?%72hYp zpj@?y6je!xM!|}xgZLKbB@(x-D$tV&Bo=bsf~#r~$1a%+L?kVE5-2{znTUXLtWEeZ zXEYgEtdk#k)m-g>9(Bb;1P30bDR9*K>dj^Q`RY0o zjvUa>9)9^F<MUFRiJJ5OpO;&}$BUa!&^icz#>M~Da&T!IKQoN=a)BamtqKTu=0 zX&lbRLq=9G5R6G%i7LO>eG zp|34Ct~d)rRt@*O`{Ht!stx_|Zg&#XRqE9RHxXCC;{eUE?fzSV_Od#{?m;x(JLY?@j-JkR$t9vGDbh!kuuIuz^G0Tdv?{^65$V><@x3{m<^t8r0Wq?;vj4VHq5%f}H zb>Yt}=c4ybG%(5wD}GoP;Wob7h}l(Jbhsf^ZL3NW)Fhua1SJ|!NF55w*~F7o5F$!4 zsX&w{EmjUthZBrfRAOidk%~C_;bD{rD}ZLQD*bdjKeM@?u$8JRbq585y0Xl3>D^2x zBAKy>g!nTtGyqOgG&`jyfg5MklvmCY#}&s96>gYxgO#@ftpw$|?j|j^o>5C?XyOJ# z3=l4BqfN;W zP^H|q5f4TBU58oDcpx;ZA-MxOM5;<78jLuW*+NNOp2fqU-A)M+h6Hj}&?p3JAravq z9Dy}6BDsLb$DVp&_r-HpzIN009n&kv(YR@H=WYZ?n=e_}{?G&4?)X3r}#=jv3r{S#&JRE?_xK5Lv3K!agR$?CJG zQE4&~SG=yg=?C;UE^y+Ae&P4)pV5Q32fm{F8}|JzyK3)wSLh+(1GiY#ImGU+i*K&JF``hK6KW6PvmdmYwQf~T4x$WcS;oHmd zl8!@QW=;<(PaKp#Sr|lzmPFK#0f^{)&3Be>`_ZyxrvwAb3+2Ag>8xdSA*;;UdA&9z zS zVnjKZWrK)TnoR>N&b+Y%KM+@?zpd>g86WhhYM>CNY{Yn&*y)8#FCrlNbyoR`o-Qal zp_^lJsh5Lr`l34%VghZ|he)}iUY}Lavw>to!GW$SuW??!C7A{)Nlv-|YynhIE>JX!6DGH3rw{;IYe&j9>HB!|tQpv)S6 z0D8g!)1`?$$S0*Rl;(LC2?8qHTBFolvWDoaN?M-R!J4W{r~o6LFlvNCHnL#ja6n_- zLaxd5xB$p93L1!(x3yD4o~quP9oD?nU+r>Ln0hZIYl|6?sxaM&ofGTLdn)xzGYF@P zRv$uuiuhZiX_K|>7nbk&dtBp>Pu#ik!WUPcxV=nHEo|I#_b2ar_O2&4ZkxXRx{c>v zG`qSmE{jtWbLWgU&yVKMTix}R$L@UM?vMP+p4t7cxqjp3`AN-+C`gmh+?vXy1*PsW2ldZocPnhX9=6GYU~44eEnbCx$i3u1q73`86eIb)uz zbDDC%*3X2lAAlzmTFO!aaW?d6iyo#;L}7G>IgKz>Ht#asZjui`;T1z*ln5=t$mTd? z)Nnph1V}=_Q%mLK(%PD;s*>a!2~&>3%`i~ct9ih96{-5(hS`I015AMMlbPfpY&pqf zD0v3m6@Y3Id^124X`! z?u{%t0xruY; z%fyD!&R0GE{K|cQ^c&}IKJe;SZ@@?Wb|7`C{HnPQ&@Qjy_jSt;gRuAj*2(Nfsbr`iQDqT1`{s zMe%;bP*K;cY9S*}9l>P$=07Mm|6%#_UoId2x8>%KmV-XS;7RXYpDVY2vMd}|9oTb8 z*>-+)fWd}mZsXeTfSfv3p1i+2^u z!2Ms=4c2o{RJ-szerLJ;&iwLBNH*wICaqR%&RBdw9CK``;)z?27TPEMelSS}~r1qH~m?`06pa>gRcSW6lpqBj(YC#(LnAtfkt zW=7ive*+gDD0oZWlDfnNPcXfDo6x%eIE)vd84CJw8wb!@(Q83ZQ(I2Z#swb(cVej= zU-W&|vZOkosj9cyXx3h1R9vmTWLdO@nJ%>HZ7oMCl@^cPs9npLg@9D*_G(V;q$UQ? zz$DB(l-hPZ)Q3YC@*Y;5;^fW40lRW#spL?ombE**0$x&UoRv-mKYhoEVCX_;kEu?u z;lwCz$1a(HNZrZ=GU^eLlAIGx2+rpkf=UJjR2LysQO^e}haZF?`Dd`esOrptYRKa* z6V3o^wHGI;hhjFj+~wU;yDI~V6$ePE)$Oc_t>pni90p=2CU8kPH^#T*1kEYsxiv4X z`feA1PvdQ$qNQt(fev(v&2_r!G@2;azOzivju-Z?f-gJ{)AIC&r@r>&(Weh@-Zi~* z@AUlqB*z&GXf(3{UnkDlb#ik16JPrLIm?e+d-dE3D-(-O+-TzJGRsAgP z)a2r+rIqCs0)jR(ziEEko-(;YzxP0>8l_&45%rKyyB#>4X28R#_Nnn0+Ay{}2 z&(hjJ?CmY6g%fqy*i-U8cFnx^aTGc(HKk!vj~P2Wen)xi&RWleM)_m6i5}oa@cb*J z9J{#41_4s>Mkjdf`^t6StqON?cYjVFrmx@uB^EtE^C1C5gh;8xXDo_>Z*86+1!$t1 zGflV%3LkW2vT@|Pp)JLlZI1{*)k1pFG{rg&J2c94!3d(teu%7Kyk11QJ!58;PsBvMRx>e6>8*vgIb}j39hx1cH@mqsJaKZ)l3M2v;FfHb3)k8cBIL{enrKlwV zRMcisCcOu-C2_8BN5w?S%#@QJL6mp>G{x^B#x8XPWx}J09>jIMVsV;_ODbi(rA~ul zRS5@_PM1T#NsVb6`tCp=6nxNCngul|PNtTCQZMW(>pZc}2&Rh?tjgVp<~sjDPqQSX z$4(iD5?(Z1ZuAVxb30GHj-x4n6r#!)DL`zw-v#}Co09B_jd%cYHWO*{#G|P^GXz}v zc*jp9T{#u>SDy#tsPNN^U2BVn${v0?E|h_&tydd1ByX(;HbmfzLWsJ7rq_WG0)D0o zCousk8=kB3$8%DZT*NqmcGyEsY-o{~GnjbrARgBkTv^Mh+2n(H&e^HsVRCAG;;!+D z!)0+{v}ya{LyJeBJ+gRWY0rf-+jdN?V)zsbV@A&dXXne7P0!u`wP$Yn%$Z$k6GM6TD21;tt^iP z{j;us?hL%=X2{VnJzu+kg=3m|Y(vnSGFvCl)F9b9fZAOb2m{Lt<(Y@;BB5XJ3HeQw z6GwH6)?wGhY`4`ne3;`h9IbZp;@9f9+O}70cYI2ZE~lm~W3w>8mH>PB@gxZW#1`~i zhdHwo+1m693<7e)8Fp(eUIF}>>(S7zh^c4ajAmhmz;(LVfRehdEwUimxDb)%)Dbb0 zC%vS|Jj6tLnm2ZafztwOQ2Wh^21XSP$uy z5x}K6*(SqT0bca5Q&SRDn4=)K3)HiM@CT&X=QhA&rKUTLig~-_X<$H5;W%}aaX#iK(E9IcMPiF0@R zigdhu=!G)F*0DO;Fi-#CM-MC>IWZcIxsjUQvS-&d*YCVw@5IXTI7^+#dEvxbJTX4* zjeBBJ=cp2g>P)ycoH(CAl9!fq2iC#_m7F$^@(@*-kwz+V({cgo^XKT3ih<*Y%c&!> zhys|&!0;1>oLE3o&5F%V*B1$8rWZhc`<}A7eLmuaXUn0d+)V&v`o9WxT09ey4Spdi`&J70N{w86T<23 ziAwxTV%LqJ_AlQ8VoMgAEkK;iO1sUy%ZA+OSlg_}E=aoKGbasoDu~0z;$(@kvNYK| z8|k9LS#^?490Li^ZfnS#7I@GfZFs)y9Y5mIkew8T$os!JXeL+(-g3HN6wq*3Es%H_ zNp_;s=ZifhCh8>p;G7QWD262^kNzZfLt}lZ74x`>lDeBwSlxz z(p(77GsB^``1^S8}cXOqcb3u&c6CPhVA*)>|rMEBYsI6f-&h8#TA(EL% zsm?Z4C=>foFo1YmwYNSA@dFtgB2zdWAUlMiiPeY7QPHmfa8q}w-fJ$pR>QpfLRooX zeC&YC&(0ot;`vj@7Unij&TX8WT=nu%f4&%$@SrRres)iCe!Q|y-gV4CU5Dm z=PP?wwk^929)DgxjUU*wwQSukVxI&cBCr1&^?Bft1Ld|qDGz@|KdU_5?y&$wVi$hR zEe6OT#0CXjLQj&+6ENo%`T-@=s(>U%Walj_aDKXMo-G?^^-*c+c~@UK^i)KcJsQMB z{9oHi+fD>@6EO#8J*zwx>19MBl`TW)`Ig-QTa}HbA9eva35xCt3RQX`a3*nK-}~M-T_pWq;Uxg4F2(D04~?SAJ89On|tTglmgPrht}7>4ekg z5U3gM-GB;CQW6yOq#EhU=3s&7#80peL*r3wVDBYTPp$OItC;SdP+A9An@yH&7ys$i z)p>Fn2zLxoZH5AA(vyUTM@^%8sW=w75j;x~*O=1w zwHh-y%6k{yQpdxHg8){@0CRv+5C*Vy>G&VaM)_vJs9i(lr2WEXN1AAXKZY$>gA^wL zsISU{Xn^g4S8zi!UOZG*PL_oe+~;s(bn4)-rISmu8z-k{C&uJrW>cBlG@9iGYwPm| zj~#sA$qUY%oI;P?4q|BXvR95Uje=8)<3o>(%HrgX?W4_e4}JCVb4I5&Z=6_NUY*?Y z=E?KkJiB%0=8IlGyX*SV>UcCUF`Aw8^PJ`J!ijQ>U3*!f&-w66P)_`MP-vKI!6x2- z*e%5f^}(PCjYQ?!MVSf!CV0Kz@^aN1*X~#JLBVJ1g>pK+@Q>q4gQccf)FJIk1e8k#%yR%p8Z53KfRNE2cyvm$cOmG zNnkqT2#-d`BEd}T1XVW$?;z+GIK4n$W9R{?IGV~9TQszr`=4p$Hi!_I1B#1OwAIV9 zKkj$7Sc=#}PLZ}=l*A1>$Q4SsW1Hs|&2`Nk`z8lQSSx zl-nAs3K@}LgeVz1&2x)FCuIHPhqr7zAxw@O#Ib_6-A;T1q|+6e!n~N+svdEl1c^6T z`|PKt6HYp%Sj6Eo0pEZG0>o%1A`%lIEJ~EeBJ^uFaJ(H%4|BOpu5ngRUNAW^LWp ztufwo9vs?!zUoUyxx?`@iqUvwyu7f&>7vh$PL}cU$xezU!th z)K7Ixo%f2VeXrAJF1SUQnbrzlS}cnv$K1gLw13sAtvJRc0?dnHps3e40 zNQF7AmF7z}g3U~oe#?*ze0%M8lxyE*`Jx|8df+DUBul{5oIZ#Vpz?+vC|AC%TySmK zb9p)Ms&dKoI!nImzbfzhZ_2xWNB^A@OnW;0QbZa0dz0ssm2cP(bP+y}D#63zKu3ls8w)$?Yzi&hx|KaQ!00ugzCSCKs4dPVLxcEnbv%*mb!wH{N zSR^8*P28iBxfhMqD6;+m*(43|ZjXy3T0>W{i(|Rs=GGH!IewDE8d{d85lxx2_vqfP&t<7C|GRkcemlAuPaIg+VU_F3DOhkkn<|-s~{Fg;5i3V-W z))Q%Z$p%XRU{+-Nf*ZlCGHa?OO2D`p@njE>V>E3GN*usdFaR;h$q_|Aro$PGMk%!a zs#vgEj6(a5O50HdC&^wy(_pzPy)^U0TZTfh2+B2){N20UMA8jUmqMqOgRfd@0$B?r z)|)Sv3o_)98s<|JYXCGom_k1LGl&#au&XiZxZ;`3UjoB$A_4GH`J#@?c0$=~#vmgo z9&6mV6&J^=`aFV8pw-pUXgr$YuW%-J&wlqF)-*|OtV$=4?9dEz?Yx8@yOrNuPa(rrW`l=61U-f={;1IXA z?4H{9?y2+MHR7gWLZ5LTFD~kSj?Kl(THlDmeOd|=ACn`-gTuxblEaY>Bq%^xMmQAG zXC*kDZJOkOP^!*S-uQj`t)u*S#L~dOGMy;WRVOPt{i$^3H!V0>Az9?8$`s5=rfWv;?Af64As5Kl z(rnCPi$XI%7n|>#bU69y;S_h49&smN4I~GT-9Q8fa_5n?A#YtLA_e7zs*3=3DQU>Z zw5{MUEduGQJ^Ln@RRbFb6;U&JSOY{FNaoR|CGAzs)s-q1>@mi9*-e-x<|x&}WS>8>oF=H*D^m^2@O>3|8AUJn3TjnNbrcRz*U4CIS zF*P@yo*Ye0FO4Uj`0As_?tXmluIUwWfkGrC#DO(6vHPNl+1b(b%;N2LPww6@8ZA%m ze8a->)`vfR)8?h;=OpwWP`x2QmOzpjP=Jh`^vGan_D$y>lK7C+JQkH$W zj&l97d(xBuSr6V^2or<>1QeXJjOWy~4%_A%ph_~(15frZiAUb}+V3oH{b7BkJn+>& zD!2WqDt++KaP+zIr4I}5yRm%v_sXaKefgvRvHZdRvwZv)^-bi5Zxvf$esg)__m^wG zLz@oL+7t%P0CljkGj*wm1z4_@N~skF zCE=ZsBfokZ{q3}f=4|VUA{nUp%m)*^??q*Msrifs+@N$z8%||&nX@X_4bU=S;z^L9 z46_EyBZ4Q)9GY^b^-{zoX`|s)5WA9fPLdW~mN}vX)j^}*<^yXX=L&IQg=M9gZ6xA#_Bwvx+r^AWq7?IMX!ivSvogWfS*2I`&kcoyQlih#1Ey& zg51?eSbfbuQmKD+(LzsGGL?C$3pYnqEn6M8GOB1*j7I)g;Ns%83(lF@G<)*s%CUu| zyYGDFuKSN2JUMaCjgKC@^U=%q&1x3mkn-=0a9g%Ip1k_%(c%I(SSwH7%le!+=aSLp z%kKHo*UsH=Z1d!)sY||R{+)k+`odS)y$Yl0`KjG+ncDjScBYl*k1pT*wy25& zkQ=8XApbM&8Bix2CCMZTbAsj$BdH)o%6S`Hy81`S>5VL<)V9$CGqvQtZO3k063v=B zKo7rWnhn90bk&TRM@sxD9@f!D4xx5v)!Ar4*eD4KP%3N>4Y-}HRqoh2MAE2F{j^;| z6we|_wY7sPTJTe1B?F{B+=(tq8MX(}f(ji@nUO}u3^oPXQYkMT^RegAGMe0|=JX6J zWaYU-J1^QYy?OTJ@s(3$G&_ClsjuGm$G`BoTR;5O&CADkZ=GC~wxC26?)2>BzP;RP zE!=kRk(W4ki zqDLCkzv}oA{RhjyS3g$1@Z0(&i|lAXbYeYI|LF7OxyQ?RS(AVv#=5#z%0pi-pZX8w zE5C0o27=vtg|ZDSFVxEdV2T^SyWr+RHmN=z>x!TZ3yB|);UAK z!o&X)i^AiJ<=B!&Sw<}gz^VV2>PM_l@eubGeV}qs=}Dl^IUGpTF+{w1#+c~`L}9{A z@<}6YYtiHpMb`vj5y;tWc6lPGTNM>xp@qwz1)dx1o)sNa>(TP*e+d-wl zH3B3}=$zx5yg{K~Gj@~$r&k-RmyEhY_Qu3!%3je$@Elw4V_M7si-@O5(?hTNo5j#6 zGd<8I9wwgWUYu0k;IT-DZUjIlJaB5dW-Bz*7+W(5NpzxlEGU`DNa1`eYF@QUR8h!8 zc$jZC5r3$FaAq=#ZjrU|@{mD~B3`w-!f$P|!;V6OIPO z>59eZiQSkMr%|)o0YXutl&;ASy?&SbP-cy58KefSE6O6b7L-;I^OKIN-coj5RK};q z$G)<9@_0FZteiZ);lgwGKYaMe(?_tj_x$NAub8`F@6^6Mljm;Hhu!OtRTxc7O;2BW z&GhB_2=deopPk$>GqLl^$=z>0xPNKKrEfg{+kaxim2aEaymPevxC-FR)M$3c{Q!Zs$JXu7;a-mVK`(J1>;f(n5Lo#)oVF&Pz;3UIS5@n*~MC1%D*S1_$?Zp zbOqGq2HJ2^E%>uZc>}^k&z=;v6%;q35X~Thfn+RnF+j2s!lX;#6LoVBpb|Dv<|5v@ z5&{)ahRlE}%|KsjNZcLgEKp1 zo;?f|6|TJtl_%m#YliR=J;Vue`K(dHS#GS-}n z;b;txrn~EuyDu)6y>>J`H$L)(GFllQdUm{UYI1&l;+%61J$3ZiM~-bdXKMF(QyVr; z&P|P`bUTiGn_E^oIW;}=uD4Il%`D#k#MJ!q>fF@id2e4Dop=1`-0sV+*|=xlh#LhB z;fn#jO^#+aPwu>M?!qhPw(Zfi<)e3&uYS@dYzV}X&|5smNrqW@_i9%}o+8aLHQ6r! zn6n8IY>j$cUh$Ul&L1zYe5V+Iul#}jjpUhG?%q0c^8zV>nb zPvp0Luw43jQBZi^mF1Q1u6XSL8NvLY$?3VW@3rNsca>MXOO*RQ=bR*Jy0e8jGuj_L zH8rsk1%I^TmFa-aD#p6aD}g5{07YrH^J~2gh$~Z$QY)&?WPxm%fwt-%HJY%IRe)Y9 z^&s&au54IzFGr4Us(eS4E=sUlM4WvY$Lb2S&(w2GO+&-i?u4kMj-PBwwINUZ}X z!|zdb%w`%Z{i-p?6^JvrX#+@F6e$7CK=l(6cm&S(IWSY+14gi4Ltf zlkJGPr0L|jSBJ?ZJaV3KoYGBy0tAtgV2eu{%%3I7ppXY{r<6CB!EgeUVM|(Q(-hJ4%H(>*Ubm~k#w88b>^_(R2?n>A-;^SIX@OJ{ zZ5`KS911<-^6+gMgUOAfsdGmgw~S`y#-quV{ZF31cl!KSojbjG?*4mDJ@MG$#H2nz zD9t?#S0*N>r!KjCa{D=>@yf*ZO}F3pX-79?MW6gwLlS|_Q1&gKqke6!2g;k@mXXDs@_cNUvhot|{H-KS0re>@ikDwrlBvFW!f z)vIm7w{E%u^(PUkKlVZXu$3U_dWR4*1ND)Fe15_64MPmQ9*gQAt4*oFSSv(tGy{o{dEow2 z2M;b!&6Uwib4D7pflY4QJazr6+1F`K?mTC9e)j8se8->tkN^ImkN@G;Q+Gb{wJ(42 zm;UW7|I>f{{HK3q^@XP+x*k@SR}MaS@`nHUx(HDlUt2kp zsbD=zH%MFp+%Ym!GaIODn0fVVP&uLFio2_8+Hv*8-!7m3wesx!>Rm;%r^1}Cb2V$* z!`x_4N@J0_H%I6K`@lKTfBE9X;j(Z8{cQ`aF=nAvv42Rs3=Tbz{MG0MR$*@=Y zEvpPrb{UX6y)4K+Smeh!vt2knl(DOnHa3#UcDB*~Z!i}M@MO})0o=<699W+x{iCir zcX0>@+C@xuC`tKU8TO705Opd#X1ud>gotJ>cs)808zIOU97Z{zV$rea3}#F6trB{2 ztqR1Jb`AR}TZ8BlELOm^dP4SAC1yMX?M&@TKCLCgp$Y?G6KUc$XSNQ=M#%`O;wGBW zfNv)jv^N291Lt5sJXxjLeeM+myN4N8^@7dkfGqm5hDtVQGKQ0J15QFF$vTXaE_fK1 zXpYk6JP@O%Rids1%^ZH}u^tD)7S43ZRhMA^k`v`B=X&GJScHLIP^zK}H$9p(YwXRd zM5lAeO^DU5oy?2^G#|Px40&Yf(%d5uZQQbs6bwTLfM`y=1HEiwne9n$u9*;zKk|Y6 zAV_at!%1lycX|mbhZCqdZ*5e#!ho(>=UEzUltYN0Jw?4!COk9&7 zYyW`|ke_CO0}VsKKhk1Gaes3reCC8T$r6$*}QFL z;pEcOkDgdu8gJb;F+Vq2W&UfOjP-nO!~FDH-Z*{X9{+2r@px)-`qo=FFCN)8b!7Y8 z;{JoxaI%7^6V{>+pe8`-TOz2 zC(CDltvviy&zLqr<0^F$Ve6+y6gJO;jF2+!W)KMTRX(orO<|OL;kU{Szo8E$nq7z$ z#!??funCJgSA%>?C(d~?iRj@0uKUjNhV~EapMJ1>^^bLH6{#(|$`x%UOm{pm`%=d=2C zj2(U->%4z zb>*8!Y{`sEx1@v^l_e;i*`aq3Cqu@46^8+rpKbPMi6*Pj1~TsK(cEcCA(XZ!N+g;s z8#)7|O{8rlAbW=#&e`A#Vb(BXjV|gH>@6#qiukQ6S3j9oi2~3m#TM2C>5{F6(4~Yt zTPj-sg2{F!OrofPBo-r4sMpaXnSkuLA`e?u2Q&fo_y*9c?a8-aaUlLzH-c`Gd=R3< zF=wLKn!+`#x(#tXg%j9w2q)QQ-cstg%5y?{k|zAx_Z@yBRmDgJlr7j?msNAtlGm7X z{l$RZqzF!xp*k*trL3x=&2@UKl>d89Fi}?qkA*VhvSVjPHen(Xwj?mUx{@O^Rg>5> zNlw4Yfw&G}&`INTp`X#o69}Bl?9Irs{lZBzrHpz;$u3d!Y*?rKG$&Hjfp~}%tQ5-0 zz>}fu5E5NxV? zEIm1%9*vJ49WQd*xHvz#ymj+6HTqR;Fgkruot2ivG>jXl`!mnyaT@`%04L zPgRUppFLP^xn*{yJaf1_c|dny`*w{kJ#TW`=BbILBdZ47kM?K_d)*7=z*t-6E z%3FT0Z1qR;0S*;ToqP)iTv^g@59Z%Ra=M&2qGNLLm`#kz-mA--en_7L3EcYyeZqe6 zxaRkB50!)dpIiGMC{H|~W9+~~jK~$rSQgwH6KNS5zi1CdI zfD#rqigcybA@d~1Pdn{6uB6ru#}LlwtnwxTKNoyjuSQBUp9)==pQA4k)trmc; z$UA>V=?{uvP!tZFiZ};> ztOM2>xq=f=IOy?5HmHl%#1*eoni8Y&D&r_yC>gk&N;N7+r6i86m2|ZVI@KINn(4~; zVnx^nD0xn3sTjD&+F6Am#Zi^U$^%5qZqZl9Q)mN%o7@#MCd$-U!=t=mSk=Z+@z6GSV|J+S!o-&lU~ z(-X7j%v|}yGgrJ*d}r$t#>xsX`!!T}dno*70g=GCVDC5KdOUriBEASH7XV z<44P@-fb17+;gMuaxS<^$JkYGDwn@m$eBU&72sQ4zD1k|pVR768Q7B_9v$1E?ouDs!I>R<9-c(qlR^3;9hE5E1HMIn%ClKH&8S$xr}{o6~WT<}WW zPMxz`ADX!GE#>upv;MvvGxaLIuUqF3%*GM5(Fssln{%EjT0Bms#N)sq^PLi5*qF8OOUhF@5W8^Zc1K%- zH8~;1#&r9hCSB6(dsCtyC&&^qTNo!#Ktj$TdbC&in9<-m`~*Ja1fW1y#be4e z2y=YEH!#U3Gz_KJ9G100%*iX;1#{+^MHpg~$Y@G~RE5oREZUOubr4R<80{3fY?9VT|BUI;LysG z`&UmK7mpan$B(Z*cWCw4$E-b$N1idTu;DGhSU;Id*jQ&Vof_ zd-LDtHmltAh4S0~QlIKhP=SO{!?fE$V2x-_Ac<2d!(bw8@A@yw_x&H-c-ip$Q{}fm zR37m0`E5T|zVH8JDl=(W5AIGL)AxKifi`cipJ)#pepVkt|IB|-J?Tve+qeI0dB=~N zseyimvHh1w0M5_-$MV@=9xfmI7vTf91bI+C$8!%w~QuoP1VIQd}6q@)tSie$slOvm$p)=5Uq4j>2x zh*6cs?rOqBNiI78Ob7-V07Mj^J&8_MV?cW1p*is_%yN}*5^B>Hh>0{2%sCMTdL-C1 zh|_H81@SXyZFc8cpY)s?DEnq9(E zos6IbqEm&F1IUedVoLR*$pVPTiYw|lJ&^RJmoig$x11 zgbCthbdZgEn(%&{s5H7152uSux)@@BI&>+tbwJKII+Vl&5Q_mRv`&Hjh*2OWWEyQ= zJ|GOx0U2%k29$UONO^O(y5X^~XzjJL0x_7!2CJ%2;zQ^Xag8}o$oQiRFZhiC?h-%r zrajG@X3rfJqPKRKtyW#{KoxOrIJTZ!&b@3je{NZLd;nZPqrbFrXr%9*&5np_VNtP_ z(d^7<`<@LuFWa;6lJlpoxN>suc~iT$Pi@~kea`&+w)rhbpWk&(Id|*C&Ml)koJQ{o z=hbLx_qM6a&zrjH{K*}gM$=Q{6VI2$EfcdBFCVyb@vh$-AAe$U$LnWb^LHk9T~?lX zr2N@0mnZJ7t_BD}9=55#2vH`4ArGka?8M2eQ6dd5es#IZ+xp9gQ^)j%&~r~{{Pw*{ z;^+i$fEy-mv$pKgZB}lZ0*~LRA8q==Z|PLSlT1Vn|42_1pWm;qZE=e>=ig-qCj4$~ z;Dx7kclG5D>o?;GCsK)me6q(;$$+vjXwpSAc&t5Y#*x&6Q(H>pa#n& zw91URtjV^&+qHr7uBRpu-)wcddU~03p}|K2J0^Z=$A{XqQHmjdAr3QR4VqWEd^T2y zpkvRS6N2)8@EB0yIgR|NIpKtdP823}^&~TP>)Gi5vLq`$t16piMYk7>T4ZS>NKO=m zM63V&RR)7ZFfk}YpT{Mbc)EYvX@{{PCd7Q^*h3ufJ1ITjg z(BF)V&eT<_S&~tg?yi1PRulFIgeaZ7bSER2G}>rPK%_}Z85#(BbI2<(8J2R3$kphN zAtTvf5Gkb}usMKOl*R-_nVvQQpd+Qnv-U7 zAZ!GJ(HP1y(Mqvw7?6rlL32jt>~~B^j|Yx2=QJ7sQJlaE5zpMS{~(YI0Eq=3KlJ+D z)v9hJZJ=wRf(l94X}!8at+PcP4VueK`VjWzuPf*5DU&m0@uAV`(b4=oI59QFy;50T z*29MRP1AeM+qmzdZC6~l^P=s$F5R*F@?E>G*t2!##`*gnoZ_S!O^g>;M&xRGdUE&H zseL=AuG~9y@ve#aY5ltH@x`S(_pk1sC>zgPJ$Uod1AjWX?V6caeQ!A%>ji|=&(Q=s#W~5;Vb4PI&J(1hBPoIiBE1o)o0xq2@civDAJT4Pu zQYXsdN!j3t8h#s^3wCY_Ije5}c)96!^-W}sE79G{3}E-s6rU&yhxH4#FrPTAceX&z zm}3X^HkIDH`n~eykCbEnsV8~vz=drdFbdRXtP15I3E}L#^bO_tC(6M`%aLcwllSOD zADR_y+^aU%A!mLolP!TTi{kZ(<4V6}F^w(GC+(izXIa&A96$CARqWIY9L&~_xU~7= zG%FKCgc4PCd$OP{i!C*|O0VzUIIHN$3Q6fsfnsSIa3=bzawEj9oEHgN82zBm(Y*N| zA6jMrxTvpDdWW|OmOY+`Va=7wj5ZFNY(`iYdY~~uw~S`D3R^o)h?KZ!CPqv|v2+>} z4mwCIy~9+5QxTb8Z1y%yQCeswSCEw3p@%_MTt5?HD2~Zz^Qb9BhA5)fQB#Hz2M~$d z67fz~_kB+`5v>+xWsDJQ_nJ~{y1W8gawLePdnIU2AiW*pD!sAN5gt$~EiR%Nl-N~W zhjCm{hOH}<&jUU3(5kwgltA^?Kwb3OT9b-zlr(|p3?NDsTa=-}Bh!|WiQKX<37cJ| z)S3~X5Q`m%pOlp!l9S9bq3kdWCl&$O@^FL{2KnjpiHZixN|?G}1Syjx1y0*;i+@rRCKl$5vlB zTuz=E&CE>fK6h&Gg%k7rU&xPES5}rEe{AKh`^(mg%F@Czw@Opf6It zF`67LEv-KN_|c`ipWSlr{@Kk}Prv1RM^lfizHs;KYkp$-s_z+{I;k&Uee!?NZmvQ8!m=F@41Z*~^~{e&F<3Bf->WyS zHt*CgQ%uilv8^ua7~|A=?y>smz7Fw0{#aaKUn!<$^(l-E=cvNDc=V7y0iR!TVrFM@ zcSwvV(SkGrS#JPM?(@WBw{eD3P!?mfnNCCibCO5(EEwR=^t)#0GbLXrlNEoIbMWiPW7b4WxX5It2@;sVWYnlz}iV>Hst zW=aV9Wb=}`A}CdK^9Ntk89hUwGr=n7&5%kffxft+13Om0>Q&2t9C4T^ksi|#CoVbJ z?KN)`Y>lxo1Ia^r!x@Or>8ctmsQkF8UaWVzi9qFP`iH&UIuMPl!sigJE(5~*I9O!z)?|LY_#FoT8<-$t#Y7NR3VG;jDd};q*xE9uB3sQ|cRrPf*JWa_RB4JuxJaM#)ZhAlYHYzj@Kz`%|R%isDx>mM7I=f_9>sGPWMymWHBqW>l`URfQV zS{$vcPHfsTv2E9A%5QE<&+6*Q6RStc#I`F&8?Gp;ODj))YGrl*XpSJXrLQclzHsQ& z<98pr=SxqG9y|D&4Xb;1ojbbdy6Gz~ow=IwyGBck<+e|iPyebvsvwot+(OORbb3-r zXR_A2lu*~3TxCd30o@_$C5^QK0BY*YjPi_xa@}_1FjE&T3azvo4XpWTJ6(1SEENr4 zdSZ(voEmsbQ374G6Txij60x>$s?ylvRcrxOzwJRzfYRW4WoGqyg~W#-^=>yxVwW@^Fbmwp0;sYeBEA=))bp{TVpa#GRSh9S3ButB7#cI_J#*-6=j%)mO06*zTKYYqzZ;1VD|gVqyg zh+UbaPGNJA>f##HRX-J^I|AYgqrse~Lnm3|+O*3ou@lD*>v8{imzQ%c98GO0eVRF}{v#?y$cEOLcoHi+(A4+&O0-YhbTQX4_lq&AZ#6%<7T&N?&9CQr-^ zMYGKg2(2@kE5+DBhFpmWnT}D%g=30kxR-?uu<4kHc(C@W-_||FRSq`vaH79H;$W2H zfP2xYg~1~9qrbfSk~gU6l0O5Vm3)^dfha#)>@19|jW{o7XgY7WgsirBzmvfVheu9M z>Nz&5@{S{7XwDNh#PJ6oP%`01M$*}H+oDSd%92Z|ay`rdw(@ux5&7v-bms&kXbzS4kj2wd2T@9zo&egQhrdhFsebF! zP`V8#s>qu@$>}UXK~E3{N+y7&io0aa;+SKetb>>s(h=z(oi63S+GJFhcm72Awh!nD*wVe@ z6Q3@p?kdaNX_)~zj;qPpGC8BStbmh8%F2dv>HmE+^4DYW@P(Vk`~P20KmNiK4H+xC+@A*0hw^{cvY!( zvO@CGq8mzeqJS4SLQDyChT621WFTV@pjSSaAx(PHo34r?7@GbLFn9hA-E_=>*$!fh zKhZrQvO&vjTM@lEKyRRWf`*ncAet#tL8|j3phtqBr6}o%63s~8lt?x_YE@MY9Jo3?SSfD z0y$IDA)aQDzBmujuZ}LgZAYl|CY2JO5sVu^>Olw~{IC%Rpi&55yn~6dpBsop3=ypJdN*JOy;tmZ0B6LBL zl>5vsdSgG0E;h@e7M`V+6DYhyVfZ`5H@3@R$gxhBtUJIiQA0>?3ppiFdm9fB4XDV9 z$QCSr0&r>-NkbMOs4d$SxHynN6hgsID#!{wr2!3Gvf&aom2LFVP>H7JtWtFL2f@f< zOU!l~kbYjglwhE#k!q6@wt$pa9Oe+E!Kn*`SlOVqW+HKU z{AfA(nX+(aSvfk^=TjqW&1H(+XZ6I%GPPrL!9N;JZPSIp@`3SFA1RN1{G-2e-#xb- z;xcz)k~=h@%%Ob6`|j_5<$J$p!`5y3p407rR=)HH<(Y?T{HT<}juxywHJ~>y z397X#R4EHdk4P#RW>*F3_3TMk=db1r&>0@26d+)pA!r(TCZ#{BfM|2g6*tsX`6|Q~ z11}}*xB`JXpDYpU6cCxO(M$#W)xHn z**=2Js1RLEbG7I?Jes?9C8w08xM#WSVnQBh3h8%Z+K(UGAW zc4!fQI;cy#mxyvBT@EB7x`-a|2Vw}#xwn(`#@_`gfqDcs$nXIJQ`I_%2;vz#QOp3O z7wk=xP-=Ix2m-dYrdIr^NSA9sV+&}w?I%Q(ptDZn+F$)~t?4&I*wm|@$`3`d?fO8> zrURV^h&e!%M&&gQgUB+?0IblYTD4h!00Q}EQ^SGSg*D=I37eHV^Ihj95k(}ocJqF4jX9Ot;F zru|70`nY6Pbc*CG8;DLIQh-{BSh+kjQV%D+!scPz=l})m&iKzf7VNgtMVYfIc2%^v zvg%`EO!nwj6W zYs1!^m%aOYU-g}TbN7W8PpvNMpFG|2vGUbV=s!*+3$h;f;!p#*9bibzlhB5Mgc20y z^qvtu5XBH{5)5h@sW2y)&0Q*^-XU`$6CI|ob!|AHX5;T_Q%^giSoL_M``iVjr-=jF zoe^orRo(P`g@wp)^@^b>ug-M3EyIj#rWZuiW*Jb&hw0`RCwZqj!*Aq@tsLGZt#$0^~XX@E~OfzqF z&pjG$jd&N2ht>~7YS?SC0EQT)lzt~cemdbvf>c=`nXlk3D3J=o5a=SA@G#1(sZhcn z69E*$MwS36IbBa2i_v&h`%k~sPI*?SZdkEQd#Zy#C|VRP*p4wushc5B?6AoJW3_9Z z>=LPG{z!?-*g~qSmEA$k6??kWQ=`Kc3jsmsa=z^ta@b4}4^USWW{A!hqJ&c`EJ`zc z&|M#D=+Mmtq76*cp{FzTOlEa;BW7Eh_NI!r%5q}8Qm@e>a8!DAC3@9#Nvl)x+67?{ zf3LeSyQ#eK{pHQyTlQXFW@pOs{_&|Vm8CmIE62)sp{y=A#L?=>rIkA$9#4%&^Hbx+ z#pMGhRu3PUotyZLkDfSqd}Eo~u<6{3uXxY=(hGj z_CN~+o*}5|a5848cTiqhxz^=Mm@5pkWaudhouOsa6}W{&9Ef0Am`3C2fD$sHWSB1T zHZy?AX>HM?>2<~eSi?*mW_&-Zyskj0vfElh)*A*bkE|1uLZpO67;~WM@Z_2!kGAt<&n}Hs9gDa{ba+` z#S z=|wiTR)7pSX;c!Fc+3pYg=UH1XIpY-0-h*Q7KB-PqR`2aIlptRY*>l%>;=Xkf-={P zBCg47L=WNs`enj80eukBxmvpn_ef!mvr0P6(0c>OMjTMZy8s~Ngk=-462LS{@F5Bi zQCx%M2-?;_i3VgN&K=ry12;igAXFyn-QR^Z@Y)JF}&-c&eN{Ql7k5&j9cFqW(#L_M^m~jKt8;ljJK25;PuR zV++p^qA+yU4Cw<%pGL?Gfhd3iqAdvx1G2-7Bilq5K21rVC8kpc0r}7v(gZb%Ug25^ zF6*ak{h&WH9*~k&hkrQLn4?-bZTPzhntiI7l$Uz-jd%_x>RDhtKs12z%~s@tO~V<% z4o#)`y^^KDC?B4h@57~ z3Q;2L6%SU};vN7IPMX5j+ade`rNSWu5yXLY8=TZygBU8=4FjQR|H>~Ww4h*Kn>L-Z znO0ocN!^8V@JyhyM_0|e=!peFhJwzx5h5Tqi_oOk)k;|t_uL?4CCaD_kdv&t)NM|s z#d2-2mOw8D5JE&Mnht2f*bqReW;^Q8OZIZt4kZr2qq6qPH?+kK>hz|oGXPOeJsx&d zDAsZwES!kbRpo(}%uG>Cfbm>v9h%U*#IDEh4na{2wq+Qo#4aHEAgeOiE!vzsiFn)& z-9Af1JcASK8-mo;s=*Bv18Tgaj4P+{``o(Z)#Wws)?L@OJ-V0D|4a3cG}q_>JFTC? zS+2-Ib>COY9iJ@+9@ST66)I7+*%PmN88tmw(l($AJdONX*?{l>h=}OTmyv*`?I}dM zZH~lsZL(_WgvpqpudPT1+6>uqZIyi3QGi4S$^;qGh zo>c>gDwwIzLjt9$S&vRMNJ*n-m!U)io+yb1psSC!C}b0f|MG`SHYsJ#m&rM<00r9~ zMtKmmyw1Lqh$81$TG@?v*QXzQM0#PMx4g`8cG}f*p@Xkpc6T{I? zQ!^wEv;bj^LjH&kZJ=89?4KF3hIzRF==>0bjZ!5so z)*X3>hQtL@fsk229X71%LXGg$;1b`=mf>qc)+0)9pM3RBO6*k z#Pl|1y3#AnZ4^%9H&5Kq*!kUJeaqgzR|yfX%p^CWnM(BFRl@J|&IVM@-gF625vRZ> zR_}NT5Kl8BTvOqSt)`A=FaTkunJ&|+5^l53yR_`Prd<50vghKmWru&Y$p3JN+o=;r z^h114-dCQur#yaFImR8BCv;2`cF$l8)uD17vU*h&gn=TC$+zA#0|1XFOjVnl0-2Q1 zReEfoONp7K$81p>h!SCMx7RpHC2AuMF(@8O;Ypw90IHam?3L=ZAuSkV>}Ge+B=Dw* zT-Jtqtsyg;x)yM@szC;*#DTaCrB*CFY)Xp1Mv3#GP~bp3p-U^X#=izpLJ7BguqQE{ zB`UDdal-3h6X^mX*d6N-RdiJcT$1N?P}yt&9{S}~(jI4qE2?L}Ib7QyoM$RP0*60< z(Qrmd$HWc>q(m>Grz<`~)M$G9Q&;nWa?OgBP{4pcF&7H`@Koid(8LoUrs;}5nJrbJ zRi#vngS_!dJ-$^9Z6-uj%5bU|i&mSUfba}m1v|5Lx*TsU*glKaESfnWb51@$r+Dlw z^@NZvj4F+uDuF7JSez~>^dN|(gl6$k?Woy3L>2tA22p+a;j1y-9W6-Za;{6!BC zN>>t|4pmPZAt0~XQCo^T$jR13Fb-D-pf}Bw@t;eZK3y~x0ElEJK^G=Vb}X`;r(1MqbpswSQo(?Tncwzba{PHFAq58L(zhkrC-*=qvEG4} zD6wwYcmis(HjSF73Guxx|lYg ze1N;1Rh>V?HE7y^B!MEFNef-E80=nk@5GkXWcSF7?v4DG3HcY@YzS&H5Syb8KR`Vw zyIFuLb2wID?(>L;#ShfR3bTs zJKAEzhh7CBcIZ{<$l*lPSZW!jB5kcnM4VOnX;9k@e-NOE9-z*a7B8I96A`gx{)a&l z0(g>kz*kG7@y}jU#X7`*3QG!Y!z6=^b)epR0`a)hk;MrBN*v{HorXa8!vURWQLoS&!(}gXd9RO@!H25KM1H=7Eh?{VfSlqOHmr1EI@MAhtZ8gw1FI z(K@j$trDmrqPwhJ@EcoOugPi@N)^`azsUSW^%X2fYqzqidE=)5=NmDo>(kl&C z5ubyBNGZQ&2Bp>$NK?~#u@Y63yhvfrDo?cl{9Uuql#Wn???RAe`_}!9&O(zCi>~2!Hgfx9Bi6&%3G&v3nt6A|oDE6fkGd z6F%C8Aot>-rvyM&lMu88;fd#nM!FeS1wUoc_wL~jh}oVaEBatu0$~86U+8-J)Y}*W zVF<*oGS)sK4@GW!s|jS_w0zpzc$(_g8=iOzBHP`L1M)nh=?ONx8V(~G;9R!gjBov+ zn5R7zq+8sl>+Q`UK!m(DZ|q%M9L5iH+ntPTq%u(`;lYm>3a1#FKyA7bfKiwo-+I>Q z-LX*I1iS1SI9SdA7tnz#(P1~HY;*<=`FD6!Q657D5B?x|3@ zO%z&BHQLddyN6KVWcDyWsxt;O!I0%4=B682curH^lz>@&@MPsjp-OKeQJ6hFfvC$G z0(q4NSI`qzp(Kr>J690piDj9oqIg3IK!=D_E$vbc8xW8KO@qKZAP|N$h29amcn53| zpy+OD$+hhXB7)j~#cfF%&q7PF>NKq}dAkX=-m`R>0volq|t1HuJ}U zgiMKbf&~(q7|cUi^XbH9B7xnLXsBLUe`Evc1%dR&fevEu%!D4uYu6)&fg9aiRRwnw zluwxwb}s+}f?#)vSB;2uQo>JA@I!>Tvq7PXIeN3KlIp(c#}j~6MhAituU=GU5X#U` zCdgq2)IYX9Z%Ghs?qrG`Zq6VROn)7$fq4kae(?7Pp>uSI!~>n zMBF9pSw#VQrMAfem11T9&V(e2i6);;50PfqYX<+B!^}XE7|qIT(||?BhScLE6qv(9 zp~SUOl*M4|f-&vrgq{ry*igANdyi((4$msO;O}*Tr=8GkscOvfuwAB64?|SJ!!nRX z6RIjJiMFc4{?P8TkeSDY5>hA>Hj&uXGWNucMerp65g4L5_M#+XEjOG2ZmKA0ha%0+ zZAyzt5Tqh;d}!}rNzRq-Na!lvC0Q?2AQY*}SPaXkb*gi;)RbChAUr@c#3dAF2*Myk zHj3KhA0;`iqRI*c(&m*6J(*8a8UoEa8pPl_oy1Q5DTk0=lu%iV-GD-P<*myhgkPT6 zE`sDda!_Jd$B*RY9(KW)AV1AQ)j(SsGTVuzO(7tLG83rixN-;*gl+~&#Yt(BU=Jbj z_K2gF&Ki+j>Y+)-fZsKh;$pjQ*bsy`!=8S9BeQ-UpDH049{c^$-D z)mCY1l{#dD9Sn#G{wTY-Q_E#xsk)KSfT%?(NHZcGsvgjqVOBy^brGX4O0z z|BU}XmSyWpt*hH*{j1{_utEW)!uB#2J%P1KC5F-}fJAClknvt)OFbI83HJh>VMrH$ z%7Na_kX|5^>eXN)i%^~fD#p{z5jb@CH3r^!C1{R#g2*s4LB@bV&1$ccz=F^tO97>W zM5J0+Jv80C0*F|VD*K7TxDkf6id}*y0S2UXu!P`H&fTS^mD8;JHsLkmkQ=@7m_^W- zTOkI#LbgLzQH{$5vk--F0+b0P2bIdEDpK;<8sH4WItZOoXo>{=i~@iHJr%TNVW`rq zpg+mXxP#eEpv&dvb6!FpPKGdaL9jBkr$&*DD)=*b=%F{nXoxq_Lp4DKkjiyBm9s*m zhB>dAQ}DILfzJ9W`kF%1g>4`p%NdtsgUj4S7N(mSjt=lA+sI193Wv}MaFP&E;lzZo z(z!@&3o}sfu>{R6O1YSZAzqT-b&yCpXV|*x?j^4|OBxu2@aS+f4B=Xo^El zfYy=Wq!N9aaBjdHw$Q65mPJF==G7twPYCr!d6R{w16BZ*K_1Ag0~|verHr>^%7^BF z0x>=Q18Z)ZBQAk@yG6k}wCn=8Xf+!puYDzhxvi-?X4m7cWFsZ!dR+Wb#tpRwegbe- zen%m$NTtiBDWTw}Jyl&sTL$7V%mFHEb#o?H(NhiCK&4}u2u%uJD^!EWi&H4>>H3kM4{5W)k}+r5>6bW>?U+b z_>->5#snLJUaOf)KS>Dp^pa4C-~fOr)rd6#&z9Xc~{4 z848l-Gqa8MUi^AFFDy4Y(!NLK;?v5E1>EPH%=-t;VTWuj&f+ z3|OPY(GX*(nTvA!A_$vGSD@o3SSg*a!I03I!J$Ixlq4lHxKmX%g{c{~b!Qa-UH;%n zTiaSl_E7vbL_N8lYF4xiU_RX+%4{OyAFPCE!vqX~7@TC;HACD6>t^S)baCn#BgAuD z(F`!~)a95z6+#8R)+vZ!G{7tq&1i_JK#32tQ+jt42eQ3V1~uuE3X3FBRpD@tcA0wg zD>-)9ayH$0+Mo!M!-2PK1YR}uTqW89Y@*9z| zFBv%;aG2F|Mn-MX>r3&LV*WLK*~KqwN7H9F(lj4=86>Bo(t6@8_zqZ{A|71 zd9^&OIUqKJ1U>4SQucg}Zf%(a@eM-(=$!$TlqeU2?RG#L785mk^!%j#Lsg~}t8VS> ziiBndmaRORwXnP9?tpDbDhtdYn(39(=Xb)P04uq=BAC8=m9vWqTJ*L1Albt$1Cd*#7fs@ z=$Sk#h_hBLK47cLJxsD|Kh8Sttu7vfAXE&=AF)kV@9ZD9*PuGz@vMOCy% z6qEt{sS}2}=X}*^Z9}#Wv63Fj7$ViE&8>{mKJX#EortVv)L^(PTi+RMH}fI3G^+=v zU3zoRgpBliq^fvRh&i#fcqs$&!4)*CG(YZj{eS{=Z}bE^16^o~+9BJJL8Wa8=yeH2 znoX~o-7mVkw8laI+sv%iIM$g}cUdO@*+`-q!V{f=bitXuf_7A)Ry#~qpKa;H`sr7U zSM}e4Pg4P`QSxHLF&}`TFVtR&%_f0x_TJO#<~$7=PnYWPH+=#{6+K1(p148c61Lb} z=@gAwM66H=ZBkUiEY0p!=}HAOGb+B6P^)!Bp`2{ZswZ@J)gz&aXteAM&}fyvnr;f= zVT3f{nq$$Ub?a?6m99B#3bvUD#%DluyB9;63U)6FA|)lwv2HPe0Mh2(N(b9k4josu z5s;`jzzfV4>6$^HI&TeeCMM;-QFYU_JGhI2O4L$@n0Q6ZZ1#WH(<2?WfOy-CLd?9E z-RO)fd9`f#lcEkuHEw9BWaXP)S-h?i#aRKi1VtrmG)WPvlsaohR9y~GrKNBKHgU!) zmx0)#%pNIJuHtY%R0KrYwg^OI-J64aplLK$ObMhp z^z5;5)xm;jhNezCtP|^}UQvb90Z3ifXK)PqkBV)((2L?}6)^E|iz##D~6^(@ZI5*CItJ(^i|j*ga!wCQVejt81kzfK*y7AccvAhGnJMc{HzOjj9_v zXfUh?B3==-tVA$8RD=A4H~c+o#8R>jd~lQ5u}GLXT_hs5BlQCLRomI^a$ngGok zOmMAAP9$OsC(45`#y3pZ)5-uvw_JW(LcMgW&n1AE0~fQA3P6boiTGKBx>$0itKX$O zG`<+DDQsa<&k9cnrn!E4i4<4d>wn$OW-0t-7 z)Y1x-w3co8V@`ni106-8h_Gg&vx(DG-8_W}a9yoVu`?MUb-Gv6#u}hAUDXw|9CUiM z54NpPQ_F1qq+io)F!F#(u{a!NSu!YO=8U!f`CNqlFGqcjbJ8>+tmRw7KK0r z)oF#9!}QXKxv;5lrGE@Wp|uY?@PZy3UYqkteq>#dC)O#>xayF5FCa(SI9AQAQp-#L zizK}jz3z04=rjx&hj(P!6zZ{6!A-q3YMcZpsds3x_{3TVoy{8Z&wr^x z7UnM=RPD`}AQgy>d&sZO&N+Z6fC08}ikZI-mI^_=P70;MpmPv56$d5}%B~HNt7ky( zN-WY~!CJB2SPV~^9m1K}DS9pt22lj!iHMM_Ga$9K+AP}tj4NGQiWX!Zh%+9VS>UPW zS3$NFAl9kYR2Wc&5~hCU6VMrC$SP=pF~>5CT5REGDB3;->!157&MH<>oD}n56U;mY z0hwri@Cgmzw|?@DrlW0?<70uVJ)`Eqv9AMzeUWCa_W%+ zsugoadD2)Jnz_bab3638Ly`t3hR6d=F51Wp;_)-qJD@6QshX0jG;C34o2ArgiF+>z!r% zlRmN19S%TFDjH#p3cI6GVh(2wtZ53HC}C)2_<_dQz-@{ucm}^pr!w^rtMzu_pWs#W zN`P88hs4t%3IQ24TQbF8>VfK#c_;-fp;)Jk6=Aaos7$S`a1fAI%dD|kk5o0a9q0wS zbs(IeAm*&1I@b8^mY!z4tCG9x8Qch!1C zjFiOziI^eIbbyka1Q3bN_4J<1s2cbXpdxyeZ_iSk}5igtAcghCaylEPI_0O-W32{Yh_o&}YN`j}GSp+aykAaTpWK(E|RY5k|2 z86Ns7Yx!;qsAXOZa5B(nmPof3&SnL1d_!$C$8Heik&394xa@WQ(uFScBpU%3l#P6L zoHpBO8zzMrbTi?rY$7>z#BH8b(Le30;YdM&NBnI+k;NA5I#8rW{wNX2(U@lF0l$vM zKXH}(3$)uQ`kG=di(1j@nwPI1P{*tQs-YIpIc16-`xNeo0>( zjp=~cYE_Wl>f5m6bf71y_!C>72bmw1P14O`(<#k>S4j)dUYlTKy%@R)>lTE8QA{`; zx)OkTa)K3DVwa>*#&j6OGZ2$+_CH#4;S>LXbOCV^(8Mw`O*w2tigpi|0BTzInB`fe+i98^{yC|m8=qcg+~)wkvYkRcYd z7*hfWQpsS!FU_q4@KX-BUcsIq)%U&tb!1!!WWs{-&0{Kt#Y7ZJzY$wB18{{CIy!4TAtVY<*I?43Se#o` z?8%FPY}q1er5sA)<8R<0Ajbx!`>SRRqGgBtSwT`BsTPi`kd5$LHVy!x#|~O zVvs90VJ5wV*^S38n66M5DIl6pSMa&rQdxw=7ThM8WKZSID%GW(@-_Flx()qSSb-N<4F(<=rh zx`T&@Sqop7)ob|##c|c4SF7g6A6KIm*;u4kPMV5-+&c?2R{{EiKy+4bjWJPOY8KZh zF2Ohuo$}Vh4I4m3#DUvkrkSpFfE0B2xh>$16K?#`hU4pEGWuw{uc>ER5+MK zl++;tz==U}pDxN&LZ?K(VmCis^4WFhK`}QGAZ{eXmF}|3MkmPHiF?|coUXNqqRo}a z#yVvkclYKWJ|W({?G>3A98@%^-~jwfQPG?S@BuoxmDe)kN@AhzeCEjk6@W6aph-N< zy%5FVN#3vFy5MQhSuG9&D3`1xk%onOhgu(Kqneh)U3SX)yN&K1oIaLp$s}uz1L=y- zk?nctQ0JA2lr`1uxPnn49#%+|=!jAWq~D%I1Eq8tsR*qf1`-&5sBnWt9YIv-%EK-t z&(SXt>(`5W6_HCp55XfCLzQ5L7&44jf8wh6Urh!?a#20;i8aI~FR0alcq(gdxIK_? z-Ta*xiVBYn`f1sXvp}TiBs}qCVwpcVr4ohFLn(&`JBX}U3zTj|suBf%rb@_cG;OB> zw8_abJ72bJFFP(Q7rm-naBbOse%Y|C%*>a`>AKPXCxUp?p?d|a>*_G%0Xo|-qQ4@n zbK}J<{^tSO7naM?iE`qF^8Eht)wfm4@IzFcUt@fj^bjkVO$TKs*BzS#f~&qjs|hLad81XsN0+ zvfaA-6l{(Pv#j-#sX;Q~e4ViiH4L_-<%O#B{ z5n6NYHjw{l5PDaMlW{=I23|EMO$=f&AK3`VZk!(|j|UVnt-3s@CZxxuY^7-vfFkny z*|1|F0m7p9}J4%99Bb> zCt_f$2wYzWu^8BXUfI1*<)&?AZlm(fYF{`}jz3?Xf2KV5XgPAItSm_hS1S=c}LlGQQ5ku%x^6-8}$E+$175$jpHf(lj1{9Rb~JuNs4rOB}kzWNHdc!po$Gw z>#6uG4DhO|GlEV|rc16XZ~Q>H^bKWh%NpCa;F}S~<Yuf_MmL^K<1sRs%v3qZMxLy$Pwah6`s#s1m!ey zCUwX34uAV+vQ?YZS;jD^R=fR;D|s7|q1tjueP&Wf6z&rzQE2k)DEEZhIy8#~nL6(`HqD>q{imui{4H7PX_hH&axoK@*=&o_J(T5{yPu>;Kf zRn76xV+ZI!wg25klf_wKkU-Ha8)q0M*|L+WSO+|kC6n>NvC|7tBKDAeC1-H_S1Fg` zX-$ExOMgs6VbDZ5WB$${isv@@=fny#`sAnU^hPAWYlb7e;SPT;I0B+HRT?;6z%p9N zX9fLGu>GN2D$pq_l{1wVUMJ?I)*FO@1BRpJqE`yfyS(hYQ0EON7L$Mz>*TR=^k6yk zWO@3*^3((6=?BW;{blK-#O;Ghy}F#;P%>|u3&*AE zAgb;!Pux`==W6#EnNX(M`3ES)icp%@24hgKgJa6 z7v)>2Z; zqv1Tql{_>h=V;Dd9a`b^-U^Yu6}G1DG)Rir9HrA4pc#ZeswC*#rlKKml6#8eAx-b; z(2^0|gWe{&+uuEcao8BBDNGSwI0Rqx&scNXz0ey0kvKgvwm`A;#yDQCrZ26dv zhqC36k)o?=Hoq9sXlmmF^gO2xTYA%#(mD+8rR=lOtyVgdNoL-mNVXA=OF~y5Mv5*> zoOxVPipNaUtCL`wjS?j}t{@sl%RhOQUTd%_5`t2Zm$1fU?Jz{0TIQ+PvM{%)?7gB~ z^Um_x?<(8QEfbi1aX7TU-2bI=|BdCzyL{MiiI=eXn<(2j_O2~gy|rBNrm}5s6*(&) z_`=iW_D__1Zz#{)r;{vRMOAbHRe6%zh(MTY#X6t8ZwCKP4U-8y*{rBlH;KuDW zEht-FS~-4n>DaNAW5>qJ%QJh<4gIejEE40l;2Xh_15Zs(P0elGykX;}=?$AlT%1)H zm*a=@gz)x1ERWn&mKWNjYP;uJnQ7)q);Y_SMay20B@IN7Qp>(Sd(fJw*sn=p0GZ4= z?X*an6`CsZ7Zn4@JO}86bNz~NUwA;7Xp5qwn(bMI*zja5}sf#^~m%-1LEzHyfwgm&nlALdG zr2;XiDB!bN^%|o9kSaI3lifr`Oprip2*9R1;1y2V7E=60L&A@h7=o_*%Qgytx*}-j zNpeh6#$rsv(4l_B-1=2@v(W@H>XN2%($1|k;c&nN?w6IW4 zas_&_EH0`^HfLta{Ct_4E3>l_1P(k>ZvE5p)juhRp3p@Bmbiu5aele}edV>^U3Oi( zrs~wGswz`0s=%&_!|zMKRUW;yoIF~kl+DB`#r35p8c^h|LPT0lbmbqoIWBN!L%Hhh zy1TmY)n#%<%;VLi!_P15e`4|R{i7%MPaZrtdGuIWUf!^OzjgifgP$VgZ^1W$eHZMV znwr|WWz&V{?mU0*c^6)A!L~i;&u-pU-@ja0EKhu0cUlkJP!|2zK$9$MD)j~|OQBc3 zHXSb|cr+W$pOR*v_h#yXFr-%+yl3fm!A+qqw7FWUsPw4pRzyc(ZWpLGyD=AxpAn~9 z{Kh_K#MU-h1iSO0Q^JV`VfOk~i4sadq|9%>1kHjix+$Zk#t`+MOfmR3P;y)WFlt9` z{^YnqN(x#IqeKv^QE0Ek@9&{U94->2gy+q+_CvZIrK*FQD2cY~=i%2C((x3Y5dOM^YIMN-~TI zm;o`^L?HUZOkLv@LHFkL0;GW#sBXF#E$fpP`jsYzqiRVaUK+xtthGvrz|35EzMTbbXY$d{JNkt5~72c_+fJIa$!mJ=s*O6}cSuDPbX>Q&OV zd9&sg@Z{a)^Z%jT`?<2XP~#6IgG8T1(^M3=-gqEp81fB+EPjgV*>dsg%Xj^QvgdMb zOk{2GKB_^pZ>7`E74`%Nt+6ZRf6u84}1yt(IpWD1ZKM%A>dFP7A1ur6K8OrZLTW zCBW#wnFXb0qlgfm!PJEcT{G4jD05ZYv}Nh6>`gNuLCXeM^8FyGUGAmKo=#WYHB9tn zqyZ6YhS*5Wb6??Qa}Pilwq+Hho_AJ2Ksxxd48`!ciUn+?QZ<-@W4H%Op}WEZaMOz$ zXb2~jpwuQPOxU+jWGLHuASPUelW@XDS$*&?6(xkKJmH&-CTyy>FV_K4)y)a8*v%M{ zQc8i@RF$qOB@-?K^pHs~^$rOQE{O7W&=)-v)su9!)D0LbRF|qdDrK?RJ*~^WX++|u z9SFpSj73b))fGlVn$%Ga48sJVTXwYalUI?(sy)Hfd<|eo2wTrcFVn@}weKwN{yXKe z*Nez;_3*>x-~GFC_uaZHl1b~T{7%`GSC;SlzVe1Ql&xDuJat@82!H*bO52bA3q1*( z+bH@AFO;u*rTqTym;3Hp)6=zqY_(TkU4Hgw%O#iS(4y<%TgvBuwcPVLI~H)hge`G` z5S`Er0CQbGm(av`B}cn1FMsP_mfe@u<7#Dj@z&dBe&tt2U;CO4suu&)CfKt={O|A* zoG%S8ZRMpQJ~Z+7ZQGX0x8NH>4lQ71Wo2Sw;_9of{^`H-G)nPL=Ro?ct^4izx){0xMPkf?$_Oq5G!s)VcV|m9r z%HQ}KI=%4V*z@J9f2^+S-&ZzmSA|pRV;?J@`&>DG{6&uk9GIEWvG@1>p3XmRBr_x-EOk_x*R}lGiIPcBm7_7e4ZPGr#}) zQwI*{5v4Tz#i2_jlS6okuD=SPwkwQ@GxUG&xqn^Xf%{(>IH*zvR##WK-`cTl+xNf! zyWaOVzkhaqqjJmL)g!l*KmJ$c$g}V||B*f&Ai~V%$lS}gP%!JVFxkR{p0t^<(L#kL zL?9aa77$34vR@GgphUS`vIh2F{gAPzM|!jAfNCR~eWz&vmZ8d48LaERKu zaDBiTe^P;h9gw{em%t9%8QqK&8Axd1rj!Hov&Rs zL`Cu14aL=nl!$0REsF***hJJ0p<1`px@prPY5|CvBn|SP6RML|(lHk(K}_I=GqlFg zA+qrkSnHM1$q!w>^EB0BYOEb3?3|K|R60FGN@!Hx_ygs-@6@N*Ij(NIt^DFImZzR7 zuYY~{;0Mcl->U305y<_!m~|3 zan_>i8K5$=p}h6S$~Es%g&BMF@X6o!&5eKbN0W~|rYD@;fxjsB5dQxOe{1X3(YN4b z1GiSSHX3ng9X)pJ@h6{NId%Ms%P*apnbil5HgL~6E>GWCR{g{{*nyna85nxoS*9m7 z8)-60a*-Qk0t*HjqD>kaITV^8P;afMbi)vH|}3`>>@K+7Ff7{U|40c4f5e3g};j4&etJ$Vq% zq%NH)C7xT5_|tvvXrjP|RJf9}D#ulPlQRw{Uw+bNNEF@@gM=o!Es8&ZB!L!>609=T z(W#TqWn?=~7md0-u{zi^G@2Ur%s&uqd(sGTfa4W!XFP=XT9T@j-81whvz>jp3Q;Z z%(K~V!8d|>OnIu*n!$(G{&P0YUwrYtiK%ISiEvkW?xAwzX)TgWt;{nL?)6#;!AwPf z*`@u|yI58}6;?PJsuBZA)G{S=Ta(rVn%NO5+B)>UJ8=7=Z$80fesx)vGD`y~ndZer zX{OAoj~lryumOc*&C=~nGyc0%z3%r}<-;l4u_{@EVRM+d;fwPUF`_mm0`XQGvG;fC zpu{Z&B?m)5hNjB&l&lMqQZIo3B0(kDh=If?v=q#%?MYxwrj2q|=?WwNB47{~s9^Iw zDvQWZdnH-4YSNQ|?fj|FqJwoJoSjYSl$izbV1s4c6Zjxum4wXl~ z)*^R+3FkC$5(0@-45Ko)xxDjd%D&ffy*yr7UbyYHxnKB&i6@`@2FDf9>z)3W!*v?8 zY`&4sAw#d9DRnkIzsYUxx8N@VIjDw}W5^%=st zWGG$HG}A2en(`I^g@MRg?bw%YR-gDSRW+pMJyc3^(zN|Z7^7yc4Bkz0YbAO^{;~(L zQX&Y-=-`wJTWr#cDnMm(kBaA#L$osj%W5|_4@67IA8ds`789~1>W87LEn~D%%2w+V zv|xegOdo~rex`(>U?|rCa=z&lLbn5;5@m-GNyu^!R@cECMI?%YYR1Qvk~TQ5k_tEx zA{@k(dAn^VY`T?bmU1B4T{(>bOfN;tUI?P)#k~q=aGH{blyoXx<^j6$cm+KskR3wU zBO39Iqgd9+=Y$61jWPtfL)SWII>rWAtE_<5_CZGq9T>%kY#;qQ3$BF6k_8%$xt}RnDx_|oPKi0YRfe(~-zEkg1aTI;!D>}nI^(j@J zdg|;uK6hH&guUradPfR4cC7ryZ|H_BpNW6@2tWQ`a!EPwJo$6#xW1t^ftMK=^Yr0S z(Sl0gTvAaHd7@l&U3u5f=t1Rpy!`y3ssH|;Pjch<635ki9g2T{QqnS_*K28mr@C!KrYzcE>xd{o)VJjz{+Zt!C;=z9gSFu(?B{^lDl>T zo8FG+>?}La*XQ~;EgpWj+;flKOnTS5bZJ96zV@~9+rM3Y_jk*!x0Ztk^@dY+$#uvP z_4woEnP+UTs6rFsI<}XC+{WPs&dhGq?`3T9!%q#2k-9Kpws1(0j;o$Q?sMnL_1~*s zwIIahrIp|Qor$~dl1@FbdD)0Fy^gJ3AJW%h?;2KuX|BT|eI46Ni1aRHpw+(xe+A&I znrQIoqmTXJAO7L$vVOB#S3IwLpPmqs>VPJg*P1pLue(b1oYavMPOn&9#j{k~2Ek;l zo$i&YwhVO?c|MCLOzwq^U4}}IE0(ZWykFA=vJd250n(~>sOSW&tk$7cpJUo3$Q~m8 zOr$*1Yfkbay0?zS760?kvEQP%2zwb6JqGl?MpqDiA+wZJbnD5Lts@)j0KL58yWDq| zIPq%IE1HH$+WMP&II_6`Hw`O-^t-;*knauaP>>N}7A40ZcZcO{iTucE&!;^#Wbi(!IV*L*qRXBF(tjzjv8;TyrW0aVh9!-fVkxoQ3;fE-$rlaq@} zOMm{^&mVZ|3EfvQh0cF<*}kv7Bho>$(G#-P+WMl(bk&wIs%MpUA9n>{6MCX*8*+sb zN?qy}PZ?Q~Xx}Ff=k=<~+mzl$^l#NNwFqh(+Da%Z@ zoJNi-=PvZcCX!NlFi5dAV^0-hd=}abkS-mK7C<%_9`#xQkSKCiVI>tfa)laQ2r3oc zC}R*0m6ra&LMs7y4TX|}N~2>bG$EC!35SzLhYe9X@O1zWiw-vQpaGPIV2d$sbaSy! z7l6W=UWc!y>Gg=R6d(<4H-jMsNwHP zkPlV`AR$pZ2>~83T>#EA3Y0+9cRNbB%E=GCewXE~0#bl_Al%792MmB%=}7a^>&n${ z(x(KUd#>DkvyO@%{6T%U7vOg1!ynd@z@+b`k;|g_d3`#AJEX6FU5^Ag=hpGOOuXtU zeM9S)j`p5I1CPr1c3=3>=6MIQ{`3PRjz(#nVjMVYxyI;H~pu7Iy&p~ z$!zqL>*zs%a@hM4%wYJ3&9gGEqrVv2Iy@^IKxz7K?%AW}i@+6ET=BC%`?K$O$2;Ek zwzpk+>7`%(@|SDJ+2FeCuKUp+{ZW|T`qsCuuC6}v$RoAmn*+w<@v&paw{6{e%{5o+ z-K?ou{c`40clp7f6E4Wo8>}u>O<~amMG14JsJ9iZ!AcY8lVg_3F_ZZWP^N+cjWx~I z8|Cb_U_YzqfQS-!0#w9lJ<+fRZC9-uf_v3$ln&D1ok&{S9Ya7|%^w+UeNk1j6gPZ| z#4!qbl7z-mc>w67h$j3AEpuLUm*TXl80_u=VIwE?F3`jZ;Wh=$I$mQ0d*AMX~PtNfh>Vty!OVc zrVXU71KyINWVSe^6>O}25~N!+BE>mJi4^@fhEz{-_?4)3ft5)}Js}9Aw(%n%wX4ZR zn+nx+z|ATd3SBuixisL4DdB+`n^NXCYKsTBc34`{!Lxa@lRGYV-l^NHT-1L<L{En$#;&%u94cbbv*_0vh4tmHW4bG&rOZgYMW?h6(Z>pJw}^N znxW9tS{|sXlru0Z2t;k73!)^bjkWDfYhCFxpjjJ>^#|xprwF!P^UwmUVUnNrToko2 zBvnB($bn`Z2BGtiM_VzNn;z&gMNlRV0jY@42SefolL5Kiv4i!1afX^SV5J2Hk)U!@ zlAVg(Defhc-MtPNf=!%u38WINhFl2-Is;wFVRx8je4`f#0|+Qr&Pmb&bhUYateo5J z_#Z^J=GjsSAz^|raEDE3=$zwv&cQNX8f$Y_uzQhmT5{<5NSEI&p6ZT4Zke02Dz|&; zOzB`Si9MF((xUk>u&OUtJ^HBbkic7S)rr-|bsc8YT!&{R{zWkn0SgP|g(Eem8WJ+n zmSzH1P>yF9{^lL}ygtn1@!}nK>UWrO1P)DPmrrFK{mjaln6BP?Cg-~5I(+F~Fbdb< zI)-5#d~x6qde^(&RU0o3n>KCwzz06S-PM))KKM5&%U2JXNV~5n^V{mX z2wKKooG%5Qz@Ci4tXnE|@>&)R+6#=C$ywV=VYVd2IuO50)lC7a6&sRZ1L`2!SpFdV z*@j|QU8ZYu@S=ku@9|W}!ru-F5t|jE>)o1c4Vr-v5sRxshSZ}Zda#&xm8c{T5I|PW z=%soEbeEzB5QBZw_pXSjiFb$~w%Dd?hC5`tZ=)cc)+-;ZKZpq|%Bb07%MVBwZ4rbh zp5{y}t^=fmOeBAVL37CyBXgr;hhb9(j7^7vOVs0V=Sf^>&Ot>5#OHvtxi04iXH~B- z835~8Go7^5+2Z#y=O%SrMH|X1XG>j;5o~E0b7DnT2PL-&L`F`qqcIoFkX)0X1e19Q zKJ%2`a}tB+$^#FSfAz1*Km3Qn&wqaH9xL18It=|K82)moEHHqjoH$kZ-E(tu z4?p~HC7lf}y67TqtvIfB?b>zNP&>ZKz;{_Xw3e2aZ@%TLV`c(#Yi7Reyi9XU`%897 zFR?Zg2cW)M1z=;jW=9TcZny2JGh`>}%+({W9ht)VUdfEe7YbAw%GO2~nv zO@OvGcd^a60o5K;-M8#^7MFrZ+YlC#3lRygHzvy!-a_NpEa zK}G@p!=rG513^hyGM;CzrygRU*V!udK@t+$AS~*)h^(80iR|_Dtg@jvsVKaQ0@d4q z1;y6F(LvH~Uxbc`W@QV;ThRoH&ctYrHbqp`S3pTUt8%Mn%|Lj%R7PL^fP*oWwrFgc z6Mm!VxYFB%xd~2hG&k9*vmq1*=s5?A_=X&|t=VsHdLrU_($rU7LO*_3|HuxQnGxn5 z>%j+wY+L=qf!W80*y%%tvvU4LF`fVmr}P`UeIOMw(1*E3P=PvAA2{z7`6nr>i;I)D z-a0~IJY0uKow7G*_TImCcsAIvV+Y68<(FUn#1l{4!A2pk6EKNHeJ3#t&A8UNH}) z+#94zoXImU%~p?5(fKCn_)KA zWH%mm1>z5h3a6OiY}*4UMiH^iGP|0fMXlTw07@rj z7Uk^7M_Q+uCo-knL{jFR{bYwpi6qp_9w~JRj86Frl0|P{ z<#!XZRI<_M2nnU*kkacf&6eg_1$#XuZg1PBUxgQ}l!;RczPj3$T+eJEO{L7Z>tcUO zaJ+ov@Wf+}=^Y@A%IVlmgD*`<{j8KUJD!!e4!i6+&UKWt@(k(gkgl@<$5oyO-gx7U zQ~D<6+2P77uYB{H-%RDc`|kUV-}sGt?zv}ry8q*-Z$`+8_4G5(oH}+yQ))C>c3k3) zHpj9i>r@<$1ucJ?j-yD=_Zny4q_kSShw2KwEm$Z_?0h8?Zbma3N_19;vD{nqD<%Ewb zWWz%xu@H3z2x7LKp$RZ_lGcwaC}%>5kxes{7E^5}y>`6P^w`ju83J_0hXbx4f*{0% zR;L-0#2#WX`B$*Pc2?0_J+NQ^LR4iea!PfqGb-0@&4<8g=K9p7!>S2kiZs!Y%xW)d z(T1m+OrrUtE9*qF$DGn)o)ZVlQ;(PBCH(~L1sCWSZ#Dl0N64_!>+2}P<`6sm%u27X zqg-@Rx$r{&*2YrV|7bb#oI*&-G#HMtLQ5F0LN|$P>v_(?crWbX?1p0NOoXn~ z54`I1j-hm7MhhIWMah}YlGgx>R>_fWazmRoMP;f9wuu|EFs zkAL!$pS=0zn}7Y+fBmzc{p`fV#7i*$XAk`z>-g~#ho3*FVWN4?xn4~{Zw=b5{Ekaq zhS@&+MJ3KEC`?LZ;bu<4NtIq<7kG|LF&h&gpv3k)mspHwMqPZ1p}3u-Q42E^R6&$Z*O6UVsoyuE z~yjh7P2Z%l!reVyM01 zi73P4SQD7j=OmCLno6`8Hf&Ql9) zGCjvk@?>Ov<%*D%OcxAUjA8Z+H8OA(5sxp$^l9v9bFcOS1~hxbU=xP{YGkFeoe+9z zMCmPNI&W6xJq9iKDh>uXt2m-I%<8y;6aGFc;ut7d&n{CdF*pR|un?(uf+}s10}a^K zVQgXaIs zNpn-ee18jw^B9WrNoKZt6aqb^2?3Nvy8%bs=zyNgiVpD&h!Wa~D>Da_p0H7O1;F#J znMyYeDz1iTMk=|IxP{XtW*b$Lk{3hxLGkj}Lqo5@v^Ah9Z`<2=Is0>HX=nzTZM=d$ zd11?HO$&wNX2zcBs7ur~KV~;L3FFm6y1^Q+uD*Q4hqY#)*Uw7q@M4sX z>mc#g#@WQr3Y~4p@46FFfen}A>Wy!FBXy3eFMQz(RM>*ud3Lz}{`((#=%L#97C47Y zv%`n;RM7v%npvl5=rx%+NhK7zSos+Q=$W+Y4TE$D0WUrQqmkX5ie$au*$DLJzxRt= zGRIKFL1f_ZI0EEDH2c%E@fjeqhCp%qf+F3S#&JT2@w`JuXeT)D{{LW=Yv7 zCBcR>ky6o~+lYWngRTRJ^c_>pmNdN8d0=zHEgRr96l^IEZKdn@@f~D1QMDF}5s;r| zqmo{fu-9p0ZGxm_12>xF#5YMWQ>K!$in<)Ldyhh|ew#UpEusRwY=gL<%%n+ zXaDPCzB`N?ICJFbvi~lJLkKLwHg6n?pw3{AJzXRY(8qJ@&oqn|7sLMYQA2imWgRM` zXAr+3o>lEjsCtPm@T^@!c(;KHXVs@a{pk?D1%FZC&|1tFKY+=+vFBYN+rir6%DCx~Mq^-@@xoo4^F8cz9*nzqfreZ-#*kbh5QaQP#cdAeY;3VhM ztUn@{T?~jD!_9?Sv`I6)E=3=JtSBQ2P;5;Q3P6)6pr~bM@L%VOI!!dh;UFFg{aZ&u z2Lf8JnVYLX_SqaEAhaYcyy%x^i%?hIns^vu7jvS7#75!I5r8-eKY>Jf-|GQ(NJ&u> zhbRH68EBLR(;s3S$U{-g4devj1YpphH`KmwhvQ&p1EAzwi8G4Dm;i$?V;9fa4}%0q z2}OGBh9Vv4%}I<-2xy?*u_|xQ$At1=D|&a$gG+C0aU24jEtQI%Bzh{S7xM+;isA`2 zt1*Kle^F5{83t4E&=dMyiK7Q~QO0rgBR?X#*v>%ZwBecbUhnjdVK}pENW|z5|FC{G zp32eZ%Km%H!6zI7JPr!L=RPe!n$U?91}zd_E1|GK|z@6EN1>y2a z?FHnCmqQQ;bM*H;B)!t#(7XbAqa=ajJYB&+7X)Af&@5(E1@y>m?~Kyit$6K6L{`k4 z5|9bIfhygJ+TlPIcm6xl1Y{Z)og%h)(*y}2wgTGC(dmN5C3Q*6i(!(oj`(62=Cks@ z81YPzGbuxSG3Ft09nuxpuwlb{-}~Ncuf29gICM>HOgeS8XA}h2mVF^n)D={WC$;Bm*66>KvL|5Xq z$J)%Wu%x@IMgMd!O^9;Fy>WngZnHuM^$f-iXs__&mQ=)VL@b=z1YJs-(VaqxP`VzE zCE{ur!Vf~q0V#S+m`JH8YzXMeX+gd0Yn)uez*_W}FPlRa>MNJsF)2 zYmqwt@I*EWB^T4~UG#?`F&&E?;u3t-6PH7*syrg-QK~IPnGeB20m5cYLVB^4v?SiH zSwlg0hb~k7h%Q*$uyIzYY={bmEoW8Wse8-Ax0NFYbp3Vy`Q_(-UTZIb$+q~yA-=T8y52J>f&Ov!&wS=HoLC`#3;q(I8>~^;y5$_rKkADoGz070 zbN0vnDC2!42Z(WMoBN( z&a}bhv_M1KnzdfqZkQ&e+|FY3RbY0{>>F{$W-CZMF16*^cbE!p)H^vN&FXvO>$`2$ zX6xyPNEeVh3=9Pnf<32G?nSfJR$1p0{}qCN*dDgnl^bGFU0Jm%l(Vzf*xgH#qdaH4 zWZjiAQaxRiFh?^)eWs)7;Uz)kPzPa)R~iu`R{DcA5(r3}5doDwy*|RZe?lBpu}g|* zqSovNGbbqgp%=u;nGl!c73ef#BDTVg+lmyP%p9X4a&HB5a{&aAuJp>il%{~1Q{9Jj zfavX3HI_~RiI{AW15`8;?j6FysdDd)y3<0*#*O7oZyNvfPmecmPDKC7anmNu>ECN% z8VBybu{`jlvgq$MJ0T+0bWk!^oHOV&msWIj*&l|0#}7&AXf(BZ=j!b2>Uey1l=OOH zpw?yfH2M(208mC%hZhq8&e|o4m82;c)N5}V4WdWZ8&H4EYhLr7_q=CrZtmWD@4fNH z8@F%Ye(?b8Y=uzv?b}z|--6SDe6Fsp&d<#3+PST62PdBQ#A}-oCEGz}T~<2)1ISnm zF(UdIH{SsWVYLXDjgtSU&S$x%m&vBVR46%koeh9(9#Qf!$6zpwx&Q8ri?%E&3AiNWriC?ng#1yddV6 z2hrX^W(Sp*gMl-N>nI%qy%Jd0o9$u4sVVkJ4!ByL0LM2!;PD>fA9yXega%UaGP}r;U z^Z=8*N~{GRrGRy+JH{v$PLzjkEw_KFoI0kX>xwJNkNw#AvdhNP)7}4|Ri7Q#aRy(k zSH!8Q@g4IoEZ8L)f)z*ZF4v#Psgoy*rdN zzXfMP?pZnGXE$!rI$=6J=U-)H*0Eo3XTzEocr~TU9-)K5*VtLBb&a(xn^JTiCz`SD zl9bn=C|0F$_0@W=+0_+{PB~XG0F5j68GoZ_y1iM2P90Pz$JKUXp)IQ>ASF$k+i`Zy z2lW~udTrGYVMD3=mx)7&lb|*wzY7IKiT3LzC2~%E@2xbI6KDgY0DAIPR=fhEUw%@_ zR$wWV1c@|i30C1-Q9RLYia+I_I{wI+=B=8>sFgN+%W>rt$x6$f9B4aF8g`^XivkF} z8tSfQM67%WVV14N**SP6DBTo#8^r%S4~*t8Ad`1(-os z)X~=5R&qd^9=Irkr(V0noHIZ#{2Cg|%0s9%d!Cmei7ATO&cuqj)I={gId_M(eM-~aui_q@k% zo2{1nZz^|vR&E4{1yB@V`;8DG1J-_orcd0gNiKGtzE}5W$W2d=U;nz5sj2mLUmVZs z|Npl5GF=MX^5Ss!-FJtFN+>JYUcUvWL+-IgqtVr`c*SHs)J~$FVN!YDXMHjxo|tvr z0a~}-&N3mj{@WIiRlUZH2DMt3wE@U#0(-~NS!G*V`F+3h`W*s-n46u}7hrQ%fq?cM zYs>D#E|w=_YPyRPH;)rTK~E0^H&hPl*)FIm9N4-u^mQ@Q<3KR3_UPigFcMAzrl~7B zyA7wXrMEKY@GwO@@wPc2=N-`yF$%;<&ZqgQGVf2Z2X$_7VsUi2)am7&M0+=G`*f5N zXC}a_?7AtJjM3jwJkC#q!V^e<%Awus4u=yy$(pXNHpilhI+)22x^}{T_8U7?wN>w$ zlgT_#4Vj+3Gz|E*=5XUzFPz43b|q}HOi}!y9_A|B^uV0whj9snJpSLkGCUIFPmZc) zEml-r>B`OkIIT+nTY{OHD_2}!zW@JN_Ua#Yj1L@G`Ndyc`S6FwPdpLEs25mdt(mUq zIlYSt(Cb}%w0G~s5B}iP-}`%`J$oee)P3b+|GGSOo8G|+6J;_*1xa`^B+&w0Ny!Ye zQRR13c3o6{_Ls}1op2tx@r&ah|1Zi5FI1yvKs0r)sQfjBpLpjx=jP^=|Fgl=)YMP? z)KBF{n?C*NPyf;{{Zi;BPx@Z=bhzrOtNzhH`bRrf7ju z-shSpU^r{jvb;*ffLiuYa81g`Rd(ArLzg_U9_eTlNqN(OACPac)hy-)E=@$@f0PKN z%UultMe0g`dJZbO>qZvA=rrc)(=`1hKcU3`)cF&|#6WimgqG7q^mG}AP_xZ(<$Ebm z`UeUDNi_!oP-q6%6q+q2!c2i0g>MtmOrGP3%U+KNvx3YAA`6`6iN{nHn`|_Lp+f{z zwyvT08Z>dCO^_rh>7lt7$B^#5sZ<^xkT7(BQ=Yg2z>pF)ATugXh63;!W=iUGfl`vk z-v9y_VZlTnd{I>Iy=X^)0hXW=>Iw|v@kJshIkReYp*(SC`Rsqx=LWgb+_h`sCx3GE zz27_Dxw8wUqBsAC&D2d?*F|Mmua)iFNAG|CB+5Ci7`CI&l^gzJ*?&h_KIIVdK%m`M z$ORwpNQ3)@Vm-YLCIrK7B!j%Rjt zO2#6kSy54n^Gl{(fPMtc%33OIi_gn0hdbZ>0n;ddtQC@t}g_mD(g(e=8 z^O^hf$qP+EPer|d?&+813Yj{~jhd#Bmpxq!3`EUI&t8q0&yRY4W7$yjybkWZL7n;Q;Y`G+oU}qm z?50nJh*O5hvdt+@rQZ_>qB$n4F?xiqo2B{_or()mGc76eOHacWm9KxQ-1y;g{CP}FTy)Xo zPyh7jhktl=3E%R-5DHH5N*f_ULUq0 zd?y!3`jo+Bhtw~Whi=rbW|Yy?#*L#7d|*5?lel9&^|RudhYfR*1!=BhJF7^D;a^AT zO497&L*<=#X63BJ==m1>)q#U5C)UKo#J9iaJ+t%vxi2QtgEyAN1#z&xa$K2P3#YEK zT+Ev2mga4rkYUms_KBh$Bum{G5%|Rk8w|eg4o@~U_S3LY)GpXsvn$c6w~}n}&;z^< z4SAQ2a1|GZr#T-z)pH{tS^EiDn3q6Pp7)9fJceKfsRQ_&jW|Ie5rxrHEuIcA=Sc3R zB%U+OiAs#=&E`lcA94-Ry4FD=BKG>~+(J;FWaYpcEtwW_!h@9X0q%+I`>H?+<~^qoc-jkvX%{<)u)GiHSQuRc`p@ zGCpZfeD#VO0Hu=BMnX}W!gV5wa5zax_>%CjEiIP^zEX}n!&r_cCuhF>z2)kwxy?Gw zhZjMmq~SXHusOuuHIy3G!IwUehDX;r9P*!)*z4bdzasSGit^%p7rph(Zyf1=)^PX# zY;+YgaINxMLy)g3~0}R=4`DJ{*`-iYX)Hy!X@qT}v{ns2in@ zklA=yI9_<J~H~CZ?t~Z8~-G}qzzyZDkL!QmkZDabZ#Suv z_bfo2AKs)gEj=;KqY=Txib4<@${%07`U7tt)F* zT5AY$W9>yCXY6OqYNd*vXfI=$$KEarQhDFwm zfD~jXt-tDls6Sf_#j*c?+xru6O_S>?6aCJdV`XJcS#wFM(om90gC*IbMq^F%SmGn}&Uxqt>_9dwa=^hbN zkaDj9DEFbJYR0De!gR$=(^r1i^zyHnF1vc#*x0)7zV#pf@pJ#n|1zCDn+^7vLq8O7 zwWtqH!vA!=Z5&9CSxZmqP zb@2uK){x_Bb90l*9WQzDoB!HhJ$#w}chj>cr+@oDO!vO0J^f^T3cjBDor>JpzG-#; zTVj(s!miDAV*O)Q%6OrtE?R41RyK-bwg}y|A;vuhfIB50lr@CPY_?LRh_p#Ls}?c6 zuC)CNv+gJib$a4UsjY3Cfo_C*Jp|-=!a0gW`Gidk)$z*i-}F5Mpo}zhV=SvH!)IC! zF6-UB-r74WsMJ%lm)#lhyA9<(Y%A%KSuY6zLXtq5t6{K`zE#eCw$nCfij7JezP@_J z^)}C)M3>dH@*2h-D!wRV5Ps=VxtXa$_`(1Ii*gV%q2<8 z{Rx^0q0I8g1b6PGH-E`BdhP9bRFtp>W9V7E*LX#;AX3Ni>{HVd59lrAP;O>!{225b3Q$*9Pp?Q+ zsd`-ccsV4Ju-JU(h7AUZcW@n|+}b^j_)^A{ioVr1+`nkT1}e{hu9(HfD$#(I{0Mvh zEA`A$s2ZC^Y)Q#$Y0=vseXc|?wmIs{oeJ(Cqfmu+VVl$WQ`1c^pAH@4&|10T@{Oyn-u~bR zw@#h%xPo*Bbsg(IJkO!<2*IfNy|It7E zvR8c3!pf@7lE?3v-tiBoCqAvZX4yMYTV#C&Mp$p?)N5$zcpn7n8u~ESdDEW~u%R;~ zGffD6d(zEmCzs*4QkRX{bn?4aY>p_CNzh+AdIEV>(7^^}i6TZj7(lDf-277~3S>>8 zeHT1Gg72)ft7ZmlMi-;VrZ0Zm>+k6?C{GBPgA7VJ7-7hrqj|Gq!m8pr3_Mrq7p?r` z9{Mht-yHCTBMgXOsz}phyrp-2L<@mHXd(Apsq8tR@zc!EtkKwgq9C}-g)WniAs}g& zHI*Eb$)pQGk^>Byr5;gTu_qdh+3Edt*9$d^&dZ z!hr)iw63~}yL{{M$G4w;8jlFbRHRP1aP74#Z+zpy@A|F-U;DK@qKWgNk52FXiRpbm zH9h=Ee~#f{BV10sDf3yC)uzko8&xwHD*J(yZ)c)!|#|j*R?XWQ=ny)vcR$|vx)TmB9{R(!6oZ3 z_p=+Vi3`xqF1ndUm)Tw+B+7PT*??Fr5XKPNG9=tDvN1VmcL8t>^LSvBOjK{34Mb_u z6bAOKCT5*g`59dsWkOQdsgpsW^k;72p=_LvmjooXU-f@a1v46Z+ujv`N4_VAXw;Tj zZ`UBC#MpB=l19?jZl!%x|I`x}lrtLROJWR>aC zmeY|gB%2tO!mg}h=9PvC1n_{kXvm&J8JI_z^clb~0Y^>1bpaGv9!Y~rEY&%;7>0x^ zL>45?8bV2YDYF0&Oy2yOt6MPj&Cvt%230BS8W>aQNZ&e`5_>|b0NJBG{55jd%cY-lSplQ zI(7ea#dD_1uAP=v7S|3Oc=7G$4<6Wj@WGw4XLmL>v|~Bj&xT}D=aip4`N^Aa)^NUn zX9hi0`K$tKYil>(aQ(Nx>6_m0_20O9=!h=c#yS0GH^2CU(}mM*e!Z}C2eM7@Mm{T( zm8NU#Wtpuc&=MWDAzRX9Rb!?P6^3I*Yu*ga#y5HHBI@v9mM+RHZ=jpS_e<_pDcxIH zh^lu*K-JUCh%OEGME&TO|1V)BpLdh4L#|EJLr6Xusc13qp&R2i7LTGmT6qhO1lS~b zO1*RSv}o+u*a&?x*Pv_(l0%-=6v7{tk_-aEFJrsV1*b~tp7RFN(TYY3au z721Q+aVVX8Jp)+-nhKXBI}CMcsAmRj2QK_f!Ad-&y~~ zU)0Z!6Du}nv_M? zV9D8xurq2b-g#r0UgeYH0gdMt)#JJWe6z?Ew>EHWqg5h z=n|k;#MTmIY5VB$4K#Md3A9nP9{Lt`{`Bq>t9#A-!B8FQ)XL2*iRXd}VK}bn&oIVB zgXNX!s+*^8`o8JrJEnt|ab361oH_UIcR%$n{>A#w{LHowEsm?Tul?G~zU#XVyy{hp zhYo4F&pkEW`~K-a{O#%SPisZ9!jwdFR$+kL{fE&(<4xaZIH&Yr1Pr zAEX!TtK0`L1fg*=l-W-!nL@SaXiNXSuYPr_eF2w*AF*~;S645)?6McU;014Z-B-Nk z)n9b@=&^<6RdusgIj(-{Z>ikb)P-Qi5h^TB)~#&3M@a9TB9}xqHlxhFswLeu+AEPQ zHM3&zjZLn&j7-AmNVTo%^yI#9Xh`xaMC!6ZGEv0bDLEd-QmB8ipaqu(;NTf^s=v;F1sf4 zviazvr~duFKlT0JkL|I)@>h<1>$fgneRaB~ryiN!^OMtC|AGEnJeb3PBx2?_K~efk zeKlo4i~*ckYLW7y42d*@8&1myryE}}z4|+*n_o3$VwBe|U8)J%WG?b9!lg}2# zdt%UB{0DX+q0F0fN>jGmIp7Uqf?}_VW3_tM=f|uvKbcat<~)tjrN6k!@C-C07DsII z>7q!?LtI4eo&oeB%y@HAGEo5gOl-~)bea=?6HZF+(B+bZYf6)3Kt(~_rchUUyG6#r zF#`jLeO%Clwh{XT(k1qI$3bx{@yw@hCHA9(-smYsVhu=Wb@e)>L=B<8+NDQHCJol? zYc`n7Fh`!@lYr`_Rk5)1r*0Qf(k)ay)(X>uHGd;!u=mB#!2HG@#6H(}Hn2WCG%E+E z7ru7-UEeX?@^bz7`}X$MnKSGDZPV3TZdp2fSnmqAx2AjEKmF1VPapY39al=+v_Xd2 z2(1Ci%2yPP;_}jGfohg4we*6NDAVOm0T)QBTw0xO`J(BLZ_uH2l%up=`<<=Lt+P*W zJbilO%+ouYn}=?k`}qZD_?t#DC=|(@{+Z(vX~3!89Zkz;#J)DawK2}&Ge-Z+L--uv z)We@%T3TK`czEs5;pGEI7MA?&8Ni{!`>MO&J-zQg>bTlk*Xm_yX+^m#WPKv7nbI%X z0`a@Vl3PHxa5}1zTg(ZN+js_GfKuY* zGcJtTVf9|#`XW(c{f5#oU1~!xNwX;AFvuJZvk^|ETIU3hjCQ1$>*B;BAL>M;E8n&W z51g3M)Oh=cV}N=zfhNa!iigrlIwigw2!3*RpVG6hM%wLzP-d!=YO}OPU7lEt#Z5pa zNixI7J~m7p`a~}%2TMUs&j4o?Bzgl;Jk}${BR?lb0&ToWq!X&?Iz_@Je~L;^m7Z0M zTdxh8yGvDEAE!vDxE-oXxv=wR`fm_d;55rYE2;N_c%~py$^e^;oQm|lt1d8IlFnI| z?ZA=gn&(Zg{QBu-U#nj+-r>nb|INq3^0KI&dUSgK&-iBoKcWBlN)C1hRFxajr$e;4 zMy&m9nnGi1^gEd z-c+$TPd_?+^cSZ0{Fr{0eSwXb<2IX&4F$7BO;Qgb&W>qQmp!#yolysFrGjpso z>1tjsqs6g?8N?E$hTsh+oGPu4q#6D?QDDY01`6a@&9~93UdT>}aUQMoeYN80QHmdg z2mP)Ss}rUxHXzaM*D4{`J08C$kflxOt)MgM%wZdWCgcqJ-#Oj+@22~IW7^vALh36jYo>1g?AIfr#2|us zR*{N+n?zY%Ao|8MDC>=LX_Sm_=a^M=8Sf4n8kIx6Mu8c-nD%;ifl3KZl(TZ!jwQCK zSV7XDNT4~e<*~_fV+IvS^`zwFwW~Ux#0@aZF-EFMK)&iwU0$9d>hS9U%iC)*O!6&| zquA3J_*KndcgM|08^|r!y&DAaNm@BDC5V?aRu&YdoLwo1*Y5fOP50_`2nNOwf%sh+sSaJPW8ei}I=%RHYJT|V z^~1pr-ZedavaQQGz`!)+a?+hEoeuH4oEhXB*ZmsE%1d@Ut`HH}hRTkEHY&S-G;vv7 zoz{*{$F7~OeerbN%cje3m=0e#tsd6TXq5493(pG4-dX!i;nF$$Hp%C(H`c)an+o-; z%-kiw^59sya7KS2jK`@@zi+zlgVV`}rt?qbe*yK9^E$~&AbDKc{J?g z?gArQAV*7}ig$sm@Mz+d+qo>o;dXW(^tnAjNM@F~JDg9QsmZp`QnE5#8Aal9o4_D< z4?l?&)Y`2P95LIgcruw8g<~b42z|eYfHGr2lW`F)cX#ZA7*K|`G24dyy*3A$lq~@_ zL6j=P(_@Ww6bi_agF415Xgn$eMCzki_IYP$csw}BQ3JEG- zKTN}|i|7-so~;u?j%I*dPj6ukxsNL)IpiiUH|Nskl^%TcQuZElAp0Jjo}KS~aT`Mx zL@H$sGzPoX9LZ>aTGsS%T~pzajAW>*X@exB5LPZN>n}Oo_QlG9N2^m0PyU3kK^~n} zSE2^isR}I$r7H|rD^g=_n%qmcH=exDvTvd^HXFBr_h!5()s>~0QRE^uQz3+apeiF> zE892@mdeK|$;{ej=9Yq%X+pUxb<7INDl5zpDa(LD37aj~U1zyPmYx=U>h15-7Fw!s zx();OI3md^6QW9w$X6_pLbDu3L34H~4SlzK%in>hxUI*qWy~qVk3dr2r3gPoyX9zf z5~ZZpz?oUH7xsnNjLD~2W%a4Nk3t5BCoaF+u4vI6~qfc)bgDD!c9ap=rO(gPUlu5lXb zwNW>3nY0OUhARCsXF1fG9cGnGrcui79>gF~KvF6uG5Fnf+>zOE`b<(!CBE^(MQ4@A z#hfr+x%3}wiAFQvlDZ~2Ps88yOl>MwK7jbsz!uPRD;%+bWxiB$MwyuMeV8%ICT*qeax+W#%lUO!Qf6C@- zputzt6f;DhYZsR}z z8(r+9k&8diS@(a?jxSKy!M#vb4a%7_b$TPoJBjg)q)S-BBk7zZCSq?lfU?r2EHl{{ zqZhfQIfv%%m#W9Ayg&($>F;S`lceU{L41*x_Au~(X0HnBjg8a8EUB*4!RR zk_m!KezFcrlw)F5g|I}9_5~Eg&FodSdChoJV#b7I&I{dq#uiXjCf3NN4=43r2yxT^ zZJarjKJGn%vXQ;|^^!`XYFZgB^-C$|t_AFiY7hXevXD zy(cKKo&Zoddr=0eyCExE#uq@PE@U9o#r2ZbiS-x!lNYj+y{u(4OtA}5)ChyKzF;}F_p2pdbn6p%FDg6nUy{HF^2$srm>)^jU}FNZk7Cxw>1#KF0l~?PF4K_ z4qVF2Uc0QZ@3xe6BS{#D_0&QMTE)H~W`#`PSQb>Zu^hX5D|-_cU4{PLoeU%)kV*{D zBQHBP&fS_OLmH`z-(w_1W+T3`iN+*Bi|F3D5)&mEW80(Wa<}(1wB1`bg5nNrAij-6 z3UyT(4P_-0uhx-Q`{vZib5c-aj0e!0EvE;ftV1q|F*8L_jwD~X6 ziaf{|0T*JN92OX76#`pxv9wBZa57mcyMp%6AZ7^{GbuVrU2cMUNiXbt-%I6;DrNHH zG8fVXl4uJ&7{?%b>XBp_7nMl$?ZN0?0}jv!S-ZIpL)g&QO&kjfHSn}G+OJ<5OY3ZG z(payr?kUTDy{jvoQ8rkW1jr{TVW4p4q(tb)W-e8AWuGNbCv}`%Lug&N_dr3pl(Jps zxoH{FK+;8?AO_^1TU}R9O#!_V2vsVS)Poqd(l08zWdUWArjP-cxjP@dl_PVSVHnIv zxG}q{@$4QEna2#J#Ip@>(ZL*MwWUvbEy6iE_J+>A|ylMkDU&KWk|WLvGW!lfXg)ZzfvBjqS%G(7MnOVfyua4^e!dUh=?mOcKma!o^ z2wM{mJ4=z??VW|4zxYzAD!H1M#73hg@mzATDd|O@ai&RWLwSWd`j!NJSjJ*Enz8G$ z8V20pp!v>T%tY9sH0mkAj&wGIPEwg5ZBrBLJfz9>h(Y(#i^=C8`(WrJfjlBm8Lx3@ zQEEQBeO_BEYlKo$C>jr? z>p&jeH{LOGXDgFW?1L8F%G^8Yr9hcplz9T6t}WQ}5FVAi9`bM|0W?SkC1tdIH%TnH z^k#1C?8S)!8St7V zl&uVRnj*R$)tC;(pvGU?M8sB(R$`N>EKLDp4asGtij6F75X3QwHEBKa z3Yu4X`|L!cVfK;EK~dZh1B45o?@#91Q&!L%9$1Dsu5na2TZ+u)_&X?-Dne5UdlbA%hFBnu8p2Sg%)1=N7jSBXZpc!JG^7bbsffaS=61*; zj9{5w^nflF17+?V?r`<@xGBrMCE(jK@3o_eQsXkGlBx8%FC^5HRkM$|E7CYC95WB@ z>6%P<7j;46a)zpxfjOl}GS;ICZQNl~*Wj&+ZQ=#mG`e1heGLT5N-z0mc_4+P1XcCo z7!6Pw)+D!mkzQ4L+|(h-VABgA-}HZloCHb_Dx*QS3&|uC4#e*gO_oZ+p-l4X>K}V1 zha?lkH%;}r0>(5jsCtm-*ifcUAeJbHy=HHSm69l#M%vP(E16iML3vyaz6z*HgPBTy z#eFZL3{TR?4mLZk+WSubuQm_;M2ynhGhk0U`q(s>hu&OYn6)e{p+)w> zWUhEPK@5`*r7~m^hPN6Rh_L60(5s1Jn&Q>nwJseZ)71!b1^etGwWiglmy?SjUIAI`-mmlt#3ce`Amhxgq<}GwFc)Hqb6 zSlIc~x6iDr3~!En6_^Zl)_XRf6O?8q5g$NMfvX`=rExqGl)IDBIOd zS67ZmK1)3u-57+KT`%t1Hysz7xVMx5l{iM(`4cD`6H5>homIIBA+!xCApimlqKSJF z5Cb;s<9(mvYgPuf)GnEW&?lDchf7OY_L@Vxrwb$^%~p2~*nn_mOU<#TLo2k>%>yTi z`NIIPR1Qvwy3Un;w8W-TjbOxF<^Ni3LG2hbDsp0SLg=$i1Mh@X3Z>tR7F0D+QPy4B zls3b4m}xT*Z(Yh_YZJ)WLYQDGcT=N5*9NC~I}_7i5zuE&_V+}#Xd&Pv0DjA|^jd_{nxlP|~@kgohsH;{(b{)24 z4#e5#1PL&3Kq1k^)9;kfR!VUUPwZ7X1W+Ignr7DDT7>})omkDyi~)1esGFiiRo+R+ z$_fmk%qlO(grr_(s$<}KI90mYY~0IelW`1YBf%i0Z|Pj!8j`4bZ=iwntO7;vZ$M_!4`%u}`z^x4bhZpvT`)DL^BP30zv=HAIsmL|R`(L0$Klns*MP-2P= z1_+SGu~L_1+76mnsYD&iGz;V)X|4T1GFghCXbbz&0$H|dOH5_ILE5EsIfrtr-d5&r zbv>U1=C9_b%U>y)yM8XFX3rXsxdkLCYj5<>OBpE`5_OScr_lR)=Vd)#8)s+ zoqtNh?~nC?kawQE!*0hF=nEjRK=Vp^J$h`ikbuJZrIFxb8|{aVI~z5UQS>rhl!-D{ z=*39x5i3KZzl5vTu&6e>v~CH_#}&TfEBkbJGij{Ms3P5@ANvZWYooT-4zg%GBU zR9&E_DjPdeVgOonOe&LZiElCqD#K3MG-aPMK@sZR)lE0rq`4$kDq#R2j8~$^Q8s*q z)7?oG88z6bRLdmYKKUS8W4<~8mf=^~bV3*~ONatC&Aus|SgSVBjMpWOD|i@M7HT0E z1!RimXam^{W`UZ!Ph#DtgMVC-vzXnb!j|y@`6N)Y)Dv(VAt{L`6P`e}L4f%C95#J^ zl(FkEpk#|*^NA&mCv!w)UTosoeOzTA^vb}ga+~CxbO@0Rf3Ka;=<|S8aiGeORre6$ zA~qXP>Za>+5+o?4T+q808~D|fg^Jc|%+zMCO)2ldl6k18V<54f%F?3$f2=VBAcxKy zV4A$+2GXlMc(F<@ZYty9&F`3K%b+uB$iSIAWJ8hZz-`-F%jtc)Z`G zD@y9gL?oHtN^&HLK{t+bF@qSSj3bgOt$I=MQa(lkfpXVHN|za8-eOh6jMNJy47s%Q zQb(`B`p>V?l&>WhUN)T}UQA|F+MNo5rtA^ER93~>rgx>>X$R&DgTehy( zy;|<+0%2A@b`Ro95d|eK8AX}JW)Acu&$-SToj!$j4nT3?u|81K%0g^f*)@dGEG3^z zcxGGOvO$@y$`+)TdC+Y1cOD>S5US!jU6LV_Qj%@yt2cn5E={^%)4aJ}cUeIL0o_J< z?qQ~)(WSJEsI*65!Wv^Y$CxfXl;pTF3BWuVhMZEz6?a*0j;Xduc0FC~3YsN)4i6ny zO9}@G_61q>t)_uyj~>KazFp|kndF@zy@bG-mk)eoqU)1<@t6 zq$#IdW?Qx)m@S$>#;LeVoHV*IP^B|8{otNvL*n{!P*K@)0}ATnN`}_BkoTCRG>#Hh zY&4%aj5Rx6yB=3UAc9@WN1+`(t3MmMdpe=e5dy`N08uOHt;_D)C;L5M4BX^|K?-Zv&7RAmL|1zX>(3RexE)7tZPm3s{X+e#~z!xq( zI)=DZSBz7yxO0$;-A7=oJ>Azdp9In`p25Co#c2r#CCYlooRca1ao33YJDz~&sx|*% z^$bC!E1)&u9EPvb-Ku1;R^Wwkn7K^3QZYlvK=gNHd^*-~1!UgC1?dT4tMp}7D>fr? z;qelyFO0}+{(B{e3xG#6)9+SFgb?_GbY(0m=<>%Z`kNO#W8thYeHaOVKc*>_;Rj@xE-9@~h$iak zxrMV2oGuK)+In3V;x@#sr5~UR4;iA&I;7kRXB_ErGw81Zb$prO+^BN&+rS_dNRodT z5-!YA=EC)qT=>TGo|t5syBT6Y85cBAiIAcBHf9Hm4L~0rdIKt1r9x=CwQS5&P;V-^ z{hhYk(kB6=G|42%1lgoC1K4}!WtOgPj|Tqg5<|MWWuTqiJ5+5T)bXMOSjxDEE*_DG zA8p2K1JK4&#D= zCJ2`={{yltJ3p4K95{((B1^A|q)$tXrF3EiG%#x*KCbfMg->X32~r7zYi94SnHK68 zWJH0sAl9@0)6Qg)nZcN{k)w4*?tW=J7EOq!8b_@P|3H5djv1JJ7eN1O{_Qq(0A;AJ z#x=6rQB7czR>oH{9#`phHc2SJI|dq$lN+f(VpTn5X+m3$>7t@3GiM9k^G9MVy=pE& zCzM7-v&niwFld%W*#IJHX8iG#CJ{$(8T2BWC<`Ob1STc*nhI3#y$E?`E3*WOie3Pf z#_Q{+EyFjAE_o#BVOjAylEbLRs_eyW{zMtG#D&(JV<659Z5SX3p~&r+`7dRKP~FBe zBuU#Br0G1`chY4q9zy}fHFh;ssLYZ`2r|*d%^NrKq$~N@PpAq|IA;>(kb}IovJXKy zJCk_DK#SjYEhS%5E*#Ae$~R)ORH`e4AaB+4LuvpWJb8;21B@9FZ|&90a60)|!84p{ zMgzLi)ggiKbYNg^bpb{9-WTI2L8vkbqVij);)!0;6uK_@*p18QLFtlNIVKR!#%vPm zAiXg|aAB6H#1bTs>}kdt#M$7X*;d}dU`884csnbFcJ$5 zzy?bsk75X*Ctk4(cvT$J%F23~bp!GAvZ1qKGe&2l;AmX+*5BYf0VNFh&Xef8(Q(5uS$t5O9pb|}~ZPl!O%w|MMATj0lv1T#CW6!cAG>bdKfZGl&m>Q*pd<^ir)NZob&L0l-bTeaMbeMeF-YA#EVZkZ?$(Nif+pc zw#Dt0)Pt6)jpOJ5MpcX%gGuD&vNU@kPTK%%7><-ZT=kToQwe$Jsl>#bdhZg6zN8OuZGes$rj}- z2Zm|Un(WI`j=ifLt1g**uFD2vb8m(dV}A*l$uSj|&88a!kks{6CW&PiwT=ENUn~_C zlF}Nq+N@?DH)L!V>78n7oLD2G&c(%e2FF4P;!df0MnuhB6q+tqtPsK}FE=xvp$arP z5QbA6KY5Ws~_V^hQ=Q0>rNu6I!pk#eeQWw_mm@?-vSf&Ky^i39VEeRMAE0rYiwvi5Pcz z1ywOdUrHcjm*Gd3Wu&=nkdo?`q3b?BNM%v`N4|`tZk8I zOKFc|)!S9Grp=P|&Be{8;l*qKFD?+51~f~@Na0!wm%U!V+rvt~PYhF0r_=@S87fi=)(0@v~yf$sEH=Exkl0%_I?g9?rX7+wCXjI#=*7Wug5eFH~A4&olFDCx{XMV}29Bre^wWN8B{jIOUTu$4e` z7%1sY#56M^)%}K5zq;g~Ned6qpj;+sB8tJ}I3|Sb%cV~gobhD_MuUKAcoGhjBT~Rc zy-8^|Lzs7!sq`%|vnPHUDAg@T3{pj{%RZ3fo-TaHSRjeXD_uixRZ~eBo*Y*#hmLkR z=msw7RDa%B$I%yvHyH{VuFRNuk_=EXE?efq({l)IuCB~dBuFK#*5&#PUe6T*w5H#z zBu|Cw1FYUuG)6CbHy3vPtbc>JjOL=XJW_gi8M!K5Lz&@S?5s*o0D6_ag%V2Hgw5lm zEQqkrY}CmcHdU6U)fN5sk4r21Z+{ORpAKF=tsI(`R;P1MPWOLsT0hg{WoUX)GDw5x zYXmkD8xxm6Exp#?0E286rbAawS3iF`@xtkG6A~hd-f1>%ossXCCu?WUz{|oU*QTFL*_12@NYPW`gems;qoH zeApPN&j3s4l4D5fA)xbQFN2F)QsUb_(JLHHnRqb=RMh2^c!|2ql(}h(fvobp)eQ1` zOFprR6C^Z<(%fPQvwH0wgY@z>_5zyf^^monL~CTHYg0j2PbLf?Ugk|P12aj8L}{5= zpK#&KcoF*QxSEmMCUJvGPjBDtsFv*sw~{&Z1E@GVR86yp47HpI#so@lsB$@4CjB~6 zqRF^oNmFKd9_my8rSv^)Mo}1>syCzVMY!(R5-379)LZ`6B!6^eF9S~wmAZVzj@KwMdEsRXC-c~B&UR=B5DgxQG!(30D3dbTtaRkV$+prO*yW@krUBp zAj|qM>JJ>84j-EiUp^f;G95gsrx~7a)(-2$qH^Hqv~oa4#S?c=zxYq4C-2oaoHUf4 zTas^3$2`y_E=WoUp5>GZ$uA$6F1ulR-j_}%0~-`%4ZUEtS5u)0>LmRU|K0S+Usb=ev;HEN0M6)M?|ZSl$7f4Ua)y z%xs#C%H++ZA7DxZ4PBilUQ&AFlbO@d?O>N!^2%6N`v3VE+P0vNLF;UOqyCp2>Nk0Ky1WsPOz7j(9C2upCk~@lT;Q?h~d0II*9lT8C z+QI42vFXrd(?Oq7JjWcqLWdK=Llx!nnog${RdPpYGH8Pt~D0D;$564+W14(@n7%!FmCISvSZnFJ=be@i|OC^9G; z3*i)K7zlzg|A}_qE&%!Ul(Yn#WC42(8TW3PI7mX|T1VetsyyIQX<_=Sd=n@R$-J`# zu}K-wm4WYruGbiu7JmkS5IWMR6tWM;*Md@BJpn+4rU-Y38=I7I6sI)+g_t?9{+pMy zwrp&Nl}ugc8YGNtI`7=@%2JYgC!=I3>+OdBT&#o>i}z^xSZ`%oJEUhLo?1AZcv|9_ z3E}za*tI&GIIE6asSo{luHvY`*Mmqdn>#z)zo=Fg7nT-wrVD4M``t9U|esH?* zw5$l1v1AIyas&!SBumRUG-|Ex3c0=V_eqXkqYnU`4A|V7p7e)V5Z@BFvZxieDg_Ho(r z?-fF|1(~7oax;+(aWj;iyN{nMx$k4DUWp+jc4GF}h zfl8k%^30)?P9?Bs7gFgLZ0KsoRc9#Hrg0`MG)eZW(9mn4N~|0n>~eF*Re~mjeptrF z)Wgxv+E7t;Hk#?G3872&Zeqal;K`eyEldvtl#&Gop!Yz1n1v7~P|yte@MBzYrSbH( zg!@sgN5+uYj9$!Ah9sFlk|tKt$hK(`PUCX7X>7??N6b7M{y&JZigS=*wDFj|9Ev)| zG8-nSEh!rZIjA=L=Oc@awQ}I_bU6pomD7>q)8*GqN3Yhgbo44!IIC6<`AI9oYLUHo zcJ!u4tUKqorwdycjrqa+COGbV_~QZD(2o5DwGj1xSJw`R95`o)$6n{5+zVd78#pAh@(90rFsVC zQN(~R%9ebNY8xn%d^BpaQ0a=$wl7Gm^ri_yj){6ru2Z%TP*D=D+s8$4vy?HDaFFT$ zAT}wW!~r@ryE1eM5cV?8)I{c%pbjeEP9XX!d6v5AWz*~b^xzIwja))TF zf%Wa3b6eBtjh&P0JEzvSPi^ize17M?3)@fegf&gCy<+(d*Q}gaTHkr-uH|3*r_+7! z)Az$QGQyFPx!Y(l-vfGUPUi+#O?@ruQ_5F->-4Jcm@Yfv({$(b(@)>|$kV@cdgX)X zRvx&peRfk1d~GE53B=v1&j=T@|IP9|D|oh&f9ALZ!!sejn9bGy>vy)=vwqn>ar&~_;YRIb72RpuZ z0G$X`-A(X^ z%ABnFkx4~4*fJ$jsRn7XK~o`whU^boMCFZ}y#i4$T{w70%K?UgIEP8llZxGUr3H2m zIVC2N+~ZE8l&PeYqkV(a6OnEbkjw62(xk*JY#IUs@n1p;gUU{*!Mc=C>-;jt#{B5h zi-J6n?%fOky^K9?W%h-#(u?Z2OU!~Ilh7LdRZ2H0u~b$rD|DKfw*Ygck(!Yxny$f7 zosO7Zkn7IE4q*m3x-_KS^<`swPOLjbnk15aPotX-?CFYeNv>b?080)k1bbblp;wNJ z(>2fE`I2v6c-8Nzrzk)W9-c02b24pnGM&7z{p9-2BkSAuo}=7(dTZyx_70T!pVTl0 z%Zm$Nd)3O;kVZRf&+cdgv{BWriRb2@iQd1n+UGg^P`LJX2Z8(=x+?N8oTcurOi zPPe^w`s)8i#}$*dz4ho*r~lKV(~mwn-F0fjd;0J%OzY=#KeNAstTElX{%k6Dhvv5Di-G=>fbVVAAztB0 zNuXBdvlb_#J*)2+5`SBc-gOTrk07v>JM~1Si3tpv76+OGW*IM1p;3lGB&LmJ%BlqJ zY#>%UFNYZ*X6ocX0#p>m8(rR@b7Hm3!{4bF;K@Xp0kkCD8-y8~=o^pAF^6pWaHKaO zRCt&&?o`y|Zid)YKGDz>FHl|dp;mgXR9R3m8B4iz)SzM?C#+J@MfaPqAR$26SA7f@ zNNE>+D)^T;u`(U>j7jusU(|s9OdMsFT{4ItmhA@1mF|~kWL-VDbNg3qz2VO-UVi<_ zjqUr+=zMx&edoao+sMg{opV~fJNav$(C{3k3XaaDaA0ZSP1mk`)zvH4u52uP`aKID z`0*ubec&gjPrpaM&dC~z6H`;O2~?@3uM;eqnqhz_ zsWN4n8fwj2HU}@8zTtnGp8u8pQHz~NPoMjN51;*|QwvXT&c-^I5Q7)}{Xgc{XCFDSc+3h`R#v|Fl`sGEfBw%MV_Y~qc@ARU~aRs6WxNFgRV7loHDf=!{t5?UBgN?=% zP*I`0HR^uk80OrRMeBj%bw37bd8eXiojoaB85$~vx)8b}W2JOYw7^~~6~ zTgt_iK(w}}B6};v>@o@9&KqS_oD4>od5BFTgCt4mt#5AYA0gi`xH~qD*~&^=UFjas z!r7&1ECAb#t_BjwC3*TPD?QBmDvG5dI<$PP5^V;>MG3R?OK<+N6Y@baM`4+vB6$&0 z$?Su8se)H$L{RF|c6;1`9C=^4wsp&^H(v7f3rDa1)S2yne(!}(pWl9RbLaHd&e`pq z3%uX*Md#9s&v`1ivAqi5cBa`DuLI!!UFlG9b@Gmvym)!_ zfPO;f@a5Cfk4=w!T%QiH4%zjvXUogYqFci&HVc(z+X1(mQj6J3)G=oFlm=#o+_s4B zoG!U5#>iC)Hta)|f_#25}zMF6pw z69hCHTT?)uE-H8>3QiHXW@B&CGXxmKSAz|Cxfw%MpcCKhK0x+P1|d8BY_kQurX@+Z zXyW~6#cxMAw4hRM+CN(I*hty@1f~vSwn`k8{O(>{G`C|Ky_9(7X;Nl>bgFrz3?b7} zz-$gu*qu_oR^pMgTN=K02`XdTC;_|6*b?JZK$-=4*;5>jCF4HWLNavGT+qlR%+3nz z>@2r;nr=F{_)XWYe9ei~tCu#HKKUy;uI_u^bnYpok%YNsEn8nm z-G2W2j~Xz;=B9v1+y6U;)zqzsTmV2K57Y{EzzFt=f zJMM#&W0U$=iSchi!hjUXoMvx`(V*%{c+O?7FEVr4)3vYY!Pb?Lyqa^Gz4f^ed)-Sp zXIY|Na_DJ9=8aE$)=sP-#}%NyzOixtLk}On?C`C(-ny{7qTk#&czC+!&S~?k_80Qa z;?5G4)Ge0Z4rVCcubWaWf!>IQ1cOTuHZ9;@(Oc}k*MtPADBL3eW4V(?TlL=Z94ri) z(|+&C*z;I`BUDPs8hXjR-LAPF9%C7dmVkV6Ai<;Pwxs65JMwU>0!n4#N_doKPfy_x z4+r3wlLLuQ^XmLDD})x3^ij~v04dobG<`Q#aeTh#jEHvGHcE)%#m8F%LS%iAVqY1Kb zY3r}q_t1*w+=PXNots`Uz3BA|SKe~Z+3kOG--SfvuRpQ+ zbtjguT;5#x)UQtO`?2Z%547LAO4=F)CFA!ym;8o`-poci*9cm5k|6DD;Li44kDvJ)pW6BKd3|fGJa)VV&6;13-;r|NouAzk3*@*;qnd)M`9@e0u!u>5)(9rxjR%teC9T>}XlNq?z@JCTkdJ_flWplyO(-VB;W5 zKx63lH3A7uxg=1Ct;`CZb=89Ibm=YNm}J5Lu*`9lO<9h~*~X~@nm9(<*kjP4W|*zb zU?t8$X;iB2Z3fohp&|R^U}w+AJY`cslZ?b(1(Bqe!e@!*&3|)*ZH-djtnx2Pw|s_) zf-U^i^)}p7(W3A!B&jBh!Gl+>KKALQ76v4_g;TTSf#`Vi1ufbpw&X=reU~fD42+>i zP}n|uD*JHwQYKC^z7*&~L#3irZwsVWBdKQVO@l7?MWX?Fbm>hp>T;Q%-wIog=uZVT zBQ{E(qz@z03NM`^n9> zJhXAXzabk1r@1C_U~ziEk)?0oxVm=v_`-$hzV}Y={(tmq!uq`{2dI%Jw^Yb^+kgKp z?}?BR882UykpR6$?InBQ@bnemtG^kzu&}&FR=m}d=vEMRV^M&nyo_n>bs zZib8}P#uk>fIZA96{v^M77&*-BmEu|b9mgHQB)~WwL$eBh5C%ay^x@%HwF&lWGHv< zxj}yw5+C6zLdXsyPDcSK*%W3reo0s3cbPe)Uhh+*M4XF6v_YwQRygVsl<>xPLBABt zaYYGQ^e#n3ol{Mp=%x*)lSHsl1P`QrPH2-*N$K>^R~nUbgiOpQ*veu{YLMiZ+_e%M zNCp>yu6Qc`qD+O*Ipp&F z=bztw{zo6#Y`vERl!q3lJB}`W^L49lII(=$_Sxy7yQW|HXVb%XO&b?HhW=iL9NJuD zr6PVer06h>2@SxYdZ-XD7k~;AebcL^Z}}VgTTnaOcRu{|AAA4$BkPV?*muaKjAmb( zAYGR>yO<3amHQfiqT1UP>~XwiZSlE>_1hK=a}0Z$zxKq&fu)6)A6uR`0DFVF_k&Mu zo?%55QXP#iMpBk#Fb3TlF4&pA_#glAR{!iHKRUR%xq0;P;UDRo z{ohUZyju%sK7Z$}o2&1t^k!vdJ!?tl9bC3x_hwSn?wvM6Dhr| zm*YH=Kqlvs$JL;2m?h~i%FGF0IjiuEjU)!WGKUtZTk5LWJ({4+m6M@RGd9*_VORc& zB`#GMSN&BNclkGnm8&0~!YeLE%@`=DXAJb3%C0tZ#D;_)fp4C}mR6@P`vW^)`fV4k zxcRM*Z2ZlSoO^KH8Fa*Rm&(e6i_;e!UH*=nR$q6`@}aGBdanAlAJU&q&)1yEI(oyw;heiT?mM^jXYV}ysWV$`EW7gwsw*RH)iLcd2re>3(ewJH|Kv|t z`#Hk)_BJOLN7pxg-5dY(pZPP3tfj4u>7D;-di&p-Hh9<52g9O8K;A?zElHpIQN4N7 zJ?uy7^=%ciaSK(mj~1xRtuZt~?pX<6wr^Cqqq``+bEsEEcLl#@lZYXag}Z^{UK+cu zpBXg346T=>+O|&j>cH@Oe?(4w_!#7=Wdtr50Mx{)&givyc)aPgp^$Nf3MH?aZ68;p zbTNdS{?R7QkmwBH;!?9g?1L7uKR?l*`2-}E;p3a;=tu(%NFXLB(J4^J7~f(G;u1*j zUPP%{a$F(Or&o5Kv92kku5i6hndq5A6@8Ltp?hvo5<15f2!E1^z9t_r7QGJ2Kh(}I zzbAnm*)t3oI0K}82x|fi2o^>p!;9WOv91z>!&mIw{Icozb&sCk{*eBlk-im#rbp6nl`2f-aq})Khlp{a2n|wl1u)7JxGijJ3oH~BL}iS22?)% zWS&FT>)tLD%!V>>7mrpqy;96O+Yg;S_l_sF8I2M%0g@yULaFqi6E>RGg_j+7dG)I> zC+TYT(G2z0w1%>jO`pUDWs`otQ;+-DJ4P(CwZ(;N4=g?tVbdAo|00YMA^K|n$vPREt#S@+G1qCA zW{?nymG9HUz(YgYXMpAp6xY*^%OyZ(`ZGWI{zy2Z$)(hYEQREza09=tNH(sntldnR zN9Az=ni)|cAwsLJ8nTU{E!)@tDJ_{XrcUPW3R8tjWM}u_51t7WtT9a!=G0exp`!nMdmV7gOaV?zh)(+a$1<4 z|0UBk&)HdA`RM8GUwwRwo_0iqrvF@fDiU9^Hy&U9Q@0*?)fLNYi`&x!zp?Y4w@iXDs8m+u_8dgrq1c8;BxF1uzrcH?yA^QP-wGF|Z;JpQkJ9r*i6KE zC1pZ)&1kT%26^nl_IpllBJZ_Sy)+M;!%8^0p{HGhYnrae=o0z()5H-TU)Q55E(x9J zRgrigtv=VaThdiB>)JCFpJU`3THw*g9{-IG-K8bRLb~F{>F^1yG43fg{T1GC`Ry~k zO4lXzc0c-iC+9XLtjxAh$R=GE$=@55-Dw5F0l+7(1N$LEyMSCd$DK- zU^nQ8D_-*5J+GnPi^OVTjo4D=z5H$)y0X6mKRwlOpYccanD5M^t^bL800oBqe?MPKpw!l56!=feN> zsSBrJNPk`(KZxSM;`F9#R{zj*)?Rek(&1%MZ=XH6dGdj+3um_%S9TVc7kP8$Q|E2z5Xvv%d1

)-dD(?9&+ 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 IM6N<$f>UB!f&c&j literal 0 HcmV?d00001 diff --git a/docs/images/ex-wpf-app-simple.png b/docs/images/ex-wpf-app-simple.png new file mode 100644 index 0000000000000000000000000000000000000000..89ba6f9b94cdfb29abcec6051b8e6c8d2ea1743b GIT binary patch literal 2850 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 literal 0 HcmV?d00001 diff --git a/docs/images/il-pointers-secondscenario.gif b/docs/images/il-pointers-secondscenario.gif new file mode 100644 index 0000000000000000000000000000000000000000..7a7fff6b8fa950be1af146a3afe924115aaea131 GIT binary patch literal 157101 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 an*L5G`tn*%krVuvfUq#JGA=c8yZaw97Iz2$ literal 0 HcmV?d00001 diff --git a/docs/images/sr-coincount-hacked.png b/docs/images/sr-coincount-hacked.png new file mode 100644 index 0000000000000000000000000000000000000000..4dcd1c792b0ae105249a2e70b01815a46ac77ca6 GIT binary patch literal 40640 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 the potential states that the game can have.

Pt~!l@fz>>Ol#{K*Bp!4e zauiM4+zXz_5R7D5L{xS1vY>Hv$^f&SgdoF`qQPYAxtfU*sXF71FrK-bI02MF&$0k5 zrpO>Qbp64|lE?g3lK3-Vg8pl^cucJU39QVs73C`3@{@%;F?Zb%mEwwg1|y?X1!~a7nIj)iMSnS0EAs?ElV(-~+0;r|g%NEK1f3C! zTp6*Wsh0pE znN!zr#}rkK=_MvA!J>qo@Q+XvQ3ios@PNUUPxf9wG_4Y(lL3nP&Q-38nMOq} z7ndPY4ZwJhUAGbsLxp3Uc@*lY(^dcjGVw9ffu(5ldqGyLvj1Ka*j>+Wy*w?XE-L1g zfZkRvbguX+H*TasHp9xQxZw($f*X*UQHkIyF(5cKJ;rDp+T*7U|s}}AO=?mQ< zzicXFD06=-UTf(ie*o`9h%&E1xLY)vO&)M7#o1V^jcylj)PI_xlJ`a#VA7IE{t>)$ zno@}%iC)BDpW6#7J-#{3dT#tsc-Jq6ZTknS&-G6|5C+ZA>vm5)aN@ZKpL*blm%j2u z@BI0l*Gyb}`{aRZ+Ow0bT6w|J?e*EITDw)Bn5ebe=t5YzFj%<|x~uTMHaXp% znw?!c-0Gh{f42YXOJ_o-Gk3-Ic6}{eJcF%qZF;&kSglPQ3=`Y9&ph@}SX<&P*V`(D z1&(<)#iX4EZ;I}(`sJ>1$*4L8f_GY9JjCq&dW{aC6+AOL!s3--`@t~3FHG+Y^;S54 zHtqxfWZvO$<3|E1ylTpUP9scsuZY4#Xtwxo+FqAm(6XwvAYk|Ct%eqY(2Qyub80c{ zxH9azI_%gVw(Sm`2}Df0DfJf{MSdr0S`ewou5p*x2#b5do@gNM2x6V8bC!W5(*5m zLoGpw@LPNoG%o$UnMX?dC5S_(V{1@Wn(ZN6jZ4gmZCaP0#}_=gzXn>N$Ktt({K=Nx zkneo4oG5_xlcECZID;^4^ciBFFG%DRPZZ4lNJAB4bZ|1Tim=^~y`(C|1js#d6Sy2S zN4t zpuo9<2H!kytEy~{s3_EZG8QGpIORS@?`QAqu8mASP6w8})Maxai6 zC#0FYosMpdGy~ zcN%OCtFnE(a@qjwoY}Gj0Uz}5lIVt8%@V&fmuuL}E^KUI9WIk?43Lr_gX8$5>!Aok zxN3^K_WB@$!$7jD?(XYXDNx*jqgLzOaGx{=^@#oE6_TACb{gt992^$y;k9$%i^@lW zb2;z^F9ndvSM*E5*V`zrt)rxcO0f#usoQeRn_a#gB!VohP39v&TRCg=61*w$bSAy>8;*t&`j48vP5k z`s8AB=g#_Ehc}qXneL!<=GE1Ur_K+0-I@93+-#^VF9j?ZckT@<{nhV$wR`&5i!XMc zcyy)PnYrbYw_bBYJG}fV2mRnQBFDjc{Y?7cB89^gjT zZexSGd?uVZj!1^*9}G`@En1|A&g@*EGrYbiK2iu`s~tY{x5A$5$%g9Enee&)SKO-) zG&hjqK0#5H;y5VC8Eq+qEVg;FCKKLis|tVuX7Jb(%rsc{(is4eNn>4&8_YouN@jDx z0U;$)BhjWKP)V&s){wUD&;aXj8=zMbH;;)xq$^4|Ezykmu%`DTSs^WYe%f{tOh*>j z1u!QPRi$L*gsi*Fu2uNSBPk%O1+rT7f>jZXTs99bMl^?AoX&zV<*@)0+UDizP$5cw znyPCBAvRwwa0&n;*3)c3kookxNgq&^noawNHnoVZA<(;W*aWXg@e?aE>F6ewO_T=k z*c`xKtEqa=Frdi}P^~mDCABW=m2!a&2++@@s@the1q&jF5)hG-pa9}@s18=vW@b<` zeY~K?R0GH@H=}-0L3M#NM61lPyjlRNkm`T(!}c}1}d zfkrG`v0)?^8q?Lr6<>g*goTZDI%pc@NF0Jv3w>9E_ z0N3-HAU(<%M#IWu*@vw##xhc6Me&h7yr>#8lCxS{EnXuvC)|!dePDSwnmC>c-Ir>s ztIHE@Pvwo^hMC31W}9zYwKTSi|L+(03C4Q8(eL*f>6aMg25$~94~uJStKA+?EHvT- z)y+$O+AXV-0eF=(*v6oS^_6~619!FFpsy7wKYre6wR>x8%PVX3R&(sls7>mi0ckPN zF|5_Flkox(O}w>oUK60e7*>8OJuY9s*%mWWyA$sF<#7G`L$fhB_oe>vFZPdp2iu9W z=TCh1PaZh@wWpi)-W4}bUU%p8!fa!3p;4dQ-dNmGpY7DA_J#SIUOhE<_A8G)_m!tk zJo3!h;}_4bc9u?Gn4PNER+so;iK(gHtFQDgoS&HqtE;#;UkL5?!WDB9b$)Vy|NESs zt6>A$-WNK%!t9Q4_`$GtChi~XGp_{dg2A$T%}z3$U~o@8;O>-zfR}lJYu*>``uXsV zpAFZ(KP(=^W;jgDht6!6+7=eB2>Wgg`)&(Ui{Z>G;q>tsH|Rrx2CR?Uej}*TfxE&x zKSgajuVZRYg~vW0&Kw=uf|NC&7nn;oy*s??Q{mp954U|Z?7Ak*?+Q~3VR9}^ZwuS@ zh5fgLgLj6xo!qmXIYuRL9s`85ushuO*7DNvFs`meClD=~nL)*EDiw(K674E9mu1%Q|nT;g(H9}kXAiNT|T1Z2~I3-+h# zK+ciKpqy3lfSV#oi*P^NWlSRXs)6eux6Wp|74I0Ix3eqL~yNG)m}0C z!~d0_*JiCq!6?`eSOl3LsZC#zYEKY67DnBu6bUq01hCtnF!*UIkjO(NHzegr!XD|7 zCYsrWq}HZcPV*~eN6tqqN~&(Ltu`HD_hB)siG^hnD4}^FCs;5RdPNG2~ZZ6fhMVNpbyjV@poXUpeVwpauNy)~! zEMQ9(;CW}rT9Rf}0Cmv!Lq%ypMyu+BIY<_uBObFJ@(lUZAaK}uE{YR5c$kn?#+INK z>argE*%OsV;A-WXgiJ0Z6Yi0UdT}ttfZW0xVe^&<`Fu$(7)iG9@`UFq(R)RHM$z9R zz`bI#!*4Wgck1nldS|LJvryZv|kaqIH zHJk|)6YY|5OoBPDAdzv@Z*oYj4q3TbUJJ%0)Iq}~TXDgNRZMQ)@AR)@`<*cn>Zj{# zCoVRo{h#rT;08Zn%+iyM+R5eq@ov4>>ml|%>=^mcL~`rv#c5hq)HPu>k2q+&-WXzG z4adZ6h{$COZaa}=&swX~?sriddo${-`rMjz0VsPxi~?3609G?=J+4T~_W-vaa6Wc#VFKJnt0pXjeF@4IpG z`a5RkW}5wr^;&zev2!Ps)Ml>kF5G+gnYG9N;EN}|{H5vIh28Vr9k<*VW_G`Nle9E zoL&(3MrciheK&{pWH|j&xNwqIAdY+a2mX`ru3rkfZwSp!vC6bD{;COrK`1|Ptb@cNtUU4K+B$yysUGcw2}3-B|FYV{C| zZ4|x1h>uz100c2`v{G>rRW7fk5NxQfrEl_tOQ72>ll~n5KnlqkD5UX1LIK>!={a4O za`BO31|e!lbRaGgZB*V!nIJ)LilElp4={u1WIh%uL@nc(OlMPSbs4b#0DPeTqQHSM zfX;|18pe<%b;J}X_mHt*H#{^tK}b;mZN)wS(10i*$Iv4I7V(%ONoWX)D*>4^8;zSc z!%XIvlP=h2cPb;(9k;9`_9e+?B_Lr?Fw?*QL8dVt544#p$gP=-LUbwlfZWkQ*$$X3 zCi`i}Jaa^}h=(bynFtgS!`LI|wjwFmB1p%bOq)$0?#g(NLkkimBo%8>2hH=3WhU!a zz{tXel<OLuqsnO(K{G3(*~#6VG~V zZYP3{S7Wgm*c~4D;zYvS)8eqv;3pON)iwMw8$C{JYomxAV~1};otdv)cW3XG_jY$5 z9AF#CcQAdgfDd(?p>H~1db3bf>ACE}204bT(dsSi>M!nPKrWo&|1&BWi8iZ}agKho zOmM+xj##cFf(Pk{Of1Lh8;o?S`asxGmq$UW&S{=I+9o?T=oVz*{J1IXt$1B2yeyy3C*N*If=Ia$?7b^A>l*ZnrzSUA(=mC5Vs_@0U{D^^1Pl^?)J2#Wu^JiK~hpr_umz6 z`^m8PHnnMY_rinW$uBTH_l?Nyz{5RwfBiH@N4`!+c=1{# zwv@Q9Q1m$2G*}1=#H|VikDWnVT3$PJ0FJ0)uZSj;k)X#rPwR(L>!cn@K{FX-t_q7Y ziA`5eR^61vK{Xu=CPYQDl>sa06lNN&>l9;1)pv3wkz9tV91YW$z@U;NNjJ2@B_(kJ z)qyj}nPTE8fb^qmZL}^Y5g)oL27q0VBJhBam75NiEXDqgItI81D?I5cLvMa= zr~R!w`cD{>Jyxf(71P0Eb@W*U^7>IBwAro_O^}aDn8b->>)=Egp8L8N^(17G44Jax zXqTGPSm-tqJ+!NUt(c9_0kRB{&WFql@#u#5llnN~5U~ar0zdOqCOi4*Y*a6C388Fh z+(4pA9HKMSKtxTt#TX8i17MvZCsPE-EXc$VYEq*GsnaG{p%+O-PWC3b$97hkyjMiB zI6zgEy`n|POVx-IJ+OMfdQ$?O;Q!8DQtX{0~ z>r&Gj@dy=A^@ZG9CN)o=*RZjpxbX?7uxHud%6gX zSditF3KXIDUgz09-17pOJiKBtWqZ7-LL~4<`9J)(!}PYn#Y4T*U+%y3P-rw)rguN~ zM^7Ao^zihy)^&GI@42$mUm1keW_@D5Hal6H+}q#zo)-?i^w@9z%iWzLcf50YcCrDp zp&J@^y&oH#iMi?C<4@EPK-iz1J9oa7dq(pg6IWVTxBMleu@2Eypns-CqEw^ z{EhIf-wKa@HoWky@cg&J6JH9aU*v9n!QT!~e<>XK4nOONZRd** zhm~_-&rRGb0<$}~>wEFrR+N%-A|QwoyVDH!{B*eY7eZ??y!358Yem2T2tM~EPD?QJ z2U@yZ?^2f85Uz59sesAKv9J}>IcsGu4M7|%%{>%6@kkL^)uc58Lv0axVm-0@~0-UF4u!dK*R899TX`9p&E zQD)Ud%`5Iq&?LJ#NeeUEMUn2wMT+1=NDW{{WRNnVm9#&^I?@FYr9fR!GN;|h6d>z_ z5>SvJ0zpPw8faJBLL@5$#oDY8Ika_q`uZrX+)<@n$-4|hE7C?m^%OwW)&aU?s$fs5 za3gejN@PSK@sCMSrPV6Ms@asNjqjOe6JFHdWG&XIqWg<7M1~0sqBBW361%CYVJk?g zFf*HKxTj?yh**@{0t|@HgNht7?7O`4auP`lM0_|&P$zd8wj)-$ighx1P0&5(kw(x#j+e>#t;-$|#Y6Pv9i z1qTc)SRr7Y;)*)eG-ODiCip0eZX%_oy<)xA!nT2d)!tQmby~k{z#}>qYn#LV5gJ9f zFuqr8al6Rpit(Xhy)#jt-x&_v*uVar{q0u_+7p-mt*4g*xe^OMAkfr`$YmEc%ni)E zexuc&obAu>*SG7vwHm(yr!p-7jy~rAf=(U`Jr{W#_c%8=C}PhNI&;V4!+?n@`e@r5 z3;9Ne)pL=Qy9nr3bP2$1cdFX8tYHI)C30?|ek&D2fNSj5jaO9^cIaEx5{gP_^8ZOT zGbLH_#Clct<(#2!3^Y`XpZ;(JSd2$G9Y-Wm$C0@BvTWkpm>&>dOs@wN^JFdu`Cy=MO6wtHuY6&1Tkj%Utb`PtY2=+ivlb;e2nZ zW|jYa2@07x3Oai9SaNLSvFfZ25AjGfsX!uiuz({bK(o~t-7dbP6PH;PjTGbZ8g~vX zwzL#WK9I!;5n{BcJc``?GvWICVWfZhKlG10)IWQovG?HfPo8_}8_zGFS-$%A>8oy> zY}V_8rGC9W-B_Ftt-1Q{dtW`%dgNFC=e<+MZoh2`+OXGyrRMZv>&|-_l18)p>@%U& zgWAUY!nw0+{o}_PYZq~zcDwyoPw|7}yLa{5Q;1`;J~>mHoe$lmaPj5Z!hNAO#V>Jr z{7>bdz=4&+P6n-Dlb{ZQh)#ioP}4lvlmqTQT3>#W8>C~8hj0FRc>2qHuPK7yiG%dY z3!&BFhX9+}JJlQE$V1`WQEEdgmZ4YN74H2scUj(;LEBe;jUV}2yn@Y(Rp7sKjGSm}nfE=~A+qA3SGg%m-yV|>r;XsBsUj>WmBTaTrYBQe^U z;$a1k-*c#_xd|HRbBv3T$41TIQ3iiBsyqW#tBuZAPMIp8A^kHd zh6QR3Mgu9IvB;A)Dnu>@&P<({?k3s5E>&hjLN}dcM@ErUHbCJ+S4U7vET|=FWV79@ z3duZZhIrO#9t0Upps+iRWLl>qW!MLHU)@BDWdZDU{ci$%*eEFvgeQ!sE?B>8;f}(a2)Rk54rz)!c0fmM6amy^QSBKh zQPRbY@4dj!1pMlq&H24ctM4zrllLCghG z#RtSErO_FSF?pi-N}yajE~UN#h!@pUG?`8XYjSHeUE!3W2$3rWlLk`KUXkyHr3xxR z+>nOn#AN^~>3LMVCF~^*_E>vG=KQc?-Yd5F%Zh%kI5An9+ZnFDrFYF8y@g%m8Aa6f zJpdZsx^G4uFCbylI5pRsS%Cif%EcPL6FsRoAFqil2onKv48#y&ZVM^fF)Mjk4E8$t9qa232! zU5YO1ASdOg2O^Ox>2-qMy9est)&L{#mAa?ITf|VtE{$L=G>`PxmRA~WuP_tVbMgUW z%lQ*Z2v>Mx!|~G10?{NH%DefuOsMIvHXIptOe5k6(m?qDawDrJOpMoaI3$ zAkSQ&UhngfW^43{jF5Z!Cnc=%pZ!=MqO7-ap%p@ILW0J)&d&DXZzJ6BKJ1YOD@XcB zr(XoM+nPH1@ZodE&dly??bz3voNNrb{aU~sXbSr4^E;Osi!Xik3-jIQZo7G+w~E^% z6!Lam+p%B&{f26NfVAJ&ZLG1nyw>aX;1Fo-$(hM*yF+89x1#$hZtRF7BGl~;&b%^M zdx_r{x(k~h{d9rCAXk}1pp3!e6FhawZ0I054!Dn?F!?==>1~bQ{+;mUf6hmOVDP&i zku+bE`@t+RdD~bX@7E<&_g;tZl8rsxbBqS&I zG(Q-M>^Yho0sEokCS+C^nWdIHPX!qi}HT9DGN2YF@JApsLNWb?aLK zueF1w^rp8h9lT@t`g{4cu?SZ%?QhkrAm1*vlR+W=cMZ2+F95s_lh6K9u%y2n$Ml`MsvC#E{5#IE5I!s|+` zszjl2Sc%WRD55-fJcDI2Z-scu%EOd6EOD=x+}=!!+9_f|l;yZYz8|1v<1$pjJbZxX z(UlF5$P9~=ZlYH7I5ck#;Bmb+sMiO~(F3?J>;rq2QnmWpfG%m)sc(^X35cITh)LmM zWTZoChMIQp>biPK&E3`2wUrhB(ixz=6dSDI?@!G_#F z+i2hAN2D`aq!U)CvI!BUTSP)yoi*xMu#7%kyla9(Vc_U8`imPTG?J3&NIV4{QFGc8 zm1lQ_5B;au!wt@Uxqs%d!HMT;GuuzEOg!_zGiQ&SJ$UQXfomq3SUD~ALu0DGFkhdV zuI<=&?8%p(`wzeO&fD7YCMjT>+`8-j#=;yIm9_fncODL{UVVO{v2E_~qc1mKJu%hb zGpp9_`vwPptlp_D)LxvJ#*(pJpINBS&GU15=g!p@cGV_t6D!ZGx|8Q7_PgY0Rh%7hVT9vAMx?# zuhsV1<9xU3o*TlUKMoK4+wkZglmC_v$B!K@p9|mpd^mfI?m-)XEIUTuu+U#pNpy3# z05@9G;fDM9hE<@~rK+P($Zd5NzL71DAZsi*0oT1dTzRKTfgf&s@~g2-2a-AD@a?Xj z;kUVBLw@FPc;qvo-wW4&kROi(PCOHy_%l9Tpssq#y>@y+S1&^!OjDj0iETWPLr&Cc0B2l1fm}0E|9EX`d<~61rdp^Cg;f@0XE^4 zx`2cxr!ml|Hd@qRJ<1^xpNv^VePJtJq98-Eazvx9Pw^2_!Rtk2WbUq2la`z;h&@sm zBJ~2G=Cn0(GA9~y{4?C-96$?NA%ff~13dQ4WwdC=Gz3pQ8MSZYIwj7g)Ig&6qaoS# z&KWh@l9noewwG1qR+-UJdXP}Hh$$$Vms*pcg_tUC08noIsu7k#-9Dp;m}3H1d5pH3 zAed|cWDq@BiXyZHh>ykOA(E>)2uUICW069{9!QSLMkR*FxDe9zjCYL)OnAse^)UZ1l(Lp4|*D7Dg zeo0tiy1s0QWJUWE9AFqzP9H(kH=xE3E9Sl87_hG@a5nJ&B54hzXPwG;0wFUQ z4QX`Q!ZMo>u*`5N@SR)npD(n`Ji%&YS5Ao%fht$J{QPQ-pI@Ei+mey9JRwRYbtW2} zN&bJKGuhzJaz?fdl76()Y&GRvt+hJ*<|M0afz4&m+eo!WV^C|L+dR#A?_vZm3H2)$ zoV(+ZZfb_QPQ^?t@D`|Qn)ob}4T%FWCLbX-o?&1kFXdP?T^!=u3|qHm*gP_w#W-iv z7%{vl)SC~1k|Rth3>>78n`jL~J(*L3(!zK}3Y$9oKL{@!SQE0_ZwM4gCuWg40{3hl+bDl3eQz{8+ zr?xFN^oemW;3C}Vv|sKaBT$*0o>s9vF#%;A<`bRi$;oEBHPJ@V^!CK$6ht~SNG@jW zPP^4?G~o+3n-QZuWlbS&w3-OY=Qsosm?F5;@GkZ9f`* z=D)%z-97qGde48gfAqV}Yi@nw(78ju|M2-^XYcv&+%>mO^Y^?~Cu$Q5jUDsNUAxb8 z>redg!|fNJx%t}3HEg8>#2nkjiC_NP&B+PWx@VVG9{7{c?zOHy*jSwZ%D?*R&ZQ%J zcZA;RTI{b%%17`^h|T#)s4OT!_vjU(HHC2-QU>%_xVGJzyE&? zPkzqI3EM%poT6DF_SdsUMQl;vGjcW|S4@zj%U)$T0&dDdyZYYn$^R2S-w3?&T=;kY zb$I3*F?7bY_i=yt;O~c*4zarHn(%Y~RoJhu&pCBC{O1Y1$;fXIK$G~bOS;Hu{T@gP1cf&0oVR`v{`2BwzzWEuJ zA?RHkRux|5-}IsI-oF`k>W3D;_OHX2|5>>4qv7NKfSVcM(0>fS_h0gxcmP=7@YJ6FZHS^yh30|o) zPzllD5t}SWqgL+q{W^H;XjHv$sw#|IrphxSfc%*Up>Qs*q7^AIn;C6|`e8^EfUUCV z?l9B59ZIE8qmY%GAva}*$zvt5Wss6IfCx@F-~w{WDz_AQL_yU9gfhoet%gm2)5C4k zfSs`qfXS@p9z{}Q06ilzB(IXNGOHCK!|it7BJP4Y&1|9>SWua*Rb6rqiGB1yBnHyO zl9d>yBd1!^69su@%OF8S7m$O;A(iGV5JhbkIdzHxxJh}BGPu2^|KDT(L?x<Mp+Ul)H$(L&7RRb%xv`$o@AYck)z*pUC!c?$aq^|Gvc&&5^M@H% z!`f=UyM|Qb@3r;#k5|g}ackoD71A-kTpJBlq@{-S3{`-6$N;N6J4QOA8iFzC zudVbhob8`K(>-;(d+OzI;dH&b%FkNpckX!3RG?l^bVOGLHtL1Em`7MM0c`Y8C%+40 zpe(%`5jSvpX`;t!ghq{*(==)l9qTk~F~f_VtkXOMY&7cJKjW-wb($EX9eW!KJ26J} zsaa6y0raeMdmJ_cLmTulQcR{V>wJ>)ag4g>&-BipUVHUK_tj&)v!@1YtC+|ej?woF zc(irL;MRzrn?b|jUL@e6$eu?F*$g!fPa2kd6XCxv_{?);yBL9O{O~;L9G{;)J$1#d zU3)q+3+?H-M!StGBDvN{Vu#q!SER;z2!HlUEbAWwypDFeJvoOLM~;r@l4G_saJBe9 zM9#hb>dNZ5^XJZ-J^RYZlP6A`IDO{K+UlyVPyMxS*RxWIx20yY$sg?>*)IO_40TbR zlwwuHwRQF$M$)oIwIM)sqyeyh?CV$Vv47Hi{ELG#uQYGH=PRFi^4Tvu)vR^z z`QY4vYbUyA2DSEleQ{fTe!jkE@6+FW{>k6|();h3%*FtOuQ4@0`HO$8j_K8CUi`|p z!pgB)W6(Ny)#;19$A9O6ec|+uZOvY7AY~cTkx3@HX!iGosowGm%3Hm8aQj9y9?~n-OKv>Sukf%ad#u zG~}pKlj$mxH?=Z=8gw?vqG5d?uxpkPvy(*t_Y?_WlSNn~vJ&PpZ7ZhOIuct=Nkrr> z$6N&nvI`0<_mkqx6e|G__3@=9nREQJ^~mhIx|mHw&RX(n=myE-K#!>(y-pp^pc|5c zIA^KKw(cZ5A|*E4GAJ%*>`#WB!OM(8ely1!duLOIe$0Vs^<#DfK8kBid@-IhHt^< zUJ-L}D1RyNLKa=y62lnbHYC}^Ra5qjm_u)s$q4x^UCv6&pp&Ub!@P@R9jma zAg!+Uko!mc``!5NZ4N)=;aD0~%G*(sTsK&+VjV$OviMW!altE|9O^6q3g7J4+OcQy zx;r}8-rhNQOZ$pz8rUUr$;)&f?Z`9T7rwjp>?7T09_hb& z%*+`&wbut`2=o&PXP@+L#HuqCy-hX0MgS=f>^OYCA|fG!BJ79C%IxZJ1R#wFOOQ<) zXpd-Y-`lzF9i3}#Zy&t5z3&HZjJjt}tsH)O<+;aJo_=KcsfT+fkDB@W1O#YZa7Tc+ zZ5p~N6lCTZ*6XpuFskm&AvpE;9aS@tf(-HAc)Qr#v3DY!M;)C~sJEq$oMDDy zKY%5yI}JY|$K{S%4V?rGEs|G4XtjIq`)grw|6t|l;QZJ6N1q<7t=4z!efohHPrYzz zW}&(N>WS$|gmbnwF;}0NYVO=|>deYZ-*{?!?cC07O`1x5gn`!0cQ+60=dX?6Li*^} z>+ODH=g#_$`A5I-%*@&2+o$_=c)sWU_ANit*>`;t*V)rA)_N~DW~Ljv_Tk&s`BFXZhjA*&Si~!Enue;jW(y@BC>}fTzA1zVX}P@FQ~D z59|E-G;A=M;jW+K+facMPls>*CU>^bx#PNU!w2XFaQvz8=qC^czt1!AR^mxkc1_clnVvIc(tQ_`5Giu|-aDQ2Z}*!Jg-8(xC`Qzk>#IEGTzy zz=|q7wZrVSA{-N~5w4CvRR+x>+G}}oBhepC8@q}s;5kNHR;ViSm~5_M6z(}7nHfGa z<$_{@WI=bKso%RFX!{3Ga;E?*0U#1ROichaBqfpVrix=O!vQGg4m^e0yh!Yw^(dK{ zF;Qz37Gw@KCB%wSNrQ=KrIi#|1LK$(P_SyZvI>HT%#bH$)uRX!x`3-Hqhu%DE~zmj zvl4_hxCkU|NYvozBn@sy&4N*3)+-Z8nIU`b31e@`ZV_{v-*Iq36t#dI1#Uwuno?B2 zws;)spaRqgl*wxW9S9ehTsMUYu!e{V1Nu$|rwS_85~M3;*35sUvP{Mi@g%7xFsMk- z2i)6S){Ex^OVUb^NM@Trk_L;Di1t`9Mm$|#0JP1e5^LezSaecULY?Qj^+cmEVgde( znojDn)YGRHM*tbA-4K_%R|M#d4l>Vo81h8$b4BbG^mfx@&_;VO)*o@khl;w8a9v9=@g-(lXIiZ*u9CyI^cn@nZewY?k4TM6Q6zeXqw)=qZ4zS|8qMau zYbNji@wuP++cTf|E0gd1k@nTMHh26WjuEaQjcvQySKc`Bj{B$H^U2N~@2}4-_Aj3A zFP-O2b_+;NOq=2o&6f`k!TNy!#disf7dl0YVzcRL8pea=Mib}J^!-1v@Uwph=h2kT zqgFbPFdX_3MUD{%XC!~4AP-}1pbS6Soz9N!JFdItn)~j(_ah(r(7W%uZ((8K!o`c{ zFI?dElJk3PyZDYamm^~vMO?u!puCQW!X=SbMQGxpPz=sN$3h?ZhyOM=8%r8~F-bL7nQLSye$ovA@9v}SAbQ}wB-FhBq7H(xmV`A6=$d9shaSstsw zpxz&}-}8aSL>p_|P4zU|QbHG8z`YwiTJuH` zb#)xTF@{A6MRXRxEgc)bd}GIvxB5Z2=jX$_ex6q}0Q;^7e>(%Te0vOBK{KlbkKMLRcZT>6=fSzJjaBk>SfaP015$^s4e)h7r z%5PZx`fqVZx7yza&`HlvSy$%3L}bo5d-(|tPG)p1!;wMqz6c6)PR-(U@1PBA&|_zS+#N7D zP#h5@s5K9WOo^@+0p;XZP9&ZPSEUeoym>}}{8A1#93SMUCZXsvoJUBp)dx5^!RP;U z`2HczY~3-;UA8|~H53t%c2d8P(+xPL$WZgbg^)r_&8XO`sb0wd6JenYz9|=56_8gQ-2h6~K zIccy!p}1VK2o)+xEQzRSg;_Ir+;^kOlW0RvvJ?P?36)|57>%k9-x{(4sB)RRrsN&n zw5t`J)*tOxm+rmxQ4%HSuVLa)C;C?%54;f=)3yrZ3jBlgKe^kdI)j3RHZh zTvB@p#2^8Ts&dTy#_;QHumo?V??LTM)TS3!Z+h3t;uZY{--F6%Z4RtFfLM9GkY|;? zy@?nZT2>%tt;oans$+T?ZP|^Oy#;W~-=GpEliFE5X77vDLN?v`2x@!sXdmLiI7c*%k?m)d5~-UBBb zp9s~s+;26)2maI0>*9=6h(dx9`dp{qAtPZ;0d7w5`Z_Mpz?)lK8Up+Z>>bYIh ztF2zQb<;0&?)s&Mz8j{-H&3?1#aHVy{ra{A>{137FAQEeUZ39Cxa#NV?K2OAmmk%h zgkw7FAahxTeN=Jj#_|pna&e|AEE36Z5kc^GPG-kZaD49k$?)z^bNdIJKN-IE8{GRD z&}@Fs3|QpyI?%Cg+rzCN4s#nlT_kUEz&=|^RTT??yS{rr6&Cifx_Xh@)I)#B0rjYX z-zNt;HMbM= z+3)-zfB)nBnHYyFZVESjF#2=+sqna-E&{YG@tjP=7k{6bI&VmJIwf`eIvA5 zGk8FEM+~@!8Hk)Ig$j9Q$gnm{T{E*&$&eMo4bNomT`LL902s)xO_4=R-aG^;%UP2vHkeKyFJ_*_QZz`)+6*@E281L#Qf#3D(S40b!^Xd)ZJsJ?(=>qzb( z^+~6RNVKkGCGf$pDlTs1B*2rX5=K;gSwI4V-HYbLZ$t_Yk*;Da1G(z{(j~QVF4>lp zBcX}i6b}*#AjYvyu38VxNwwM7@g$W+*eMw{FZu#b%d((G+XW|?0kMLC6p;`OOj?{5 zQNMW5gffg`c0rX-ipXGN0@y%@e()fM$TO&42h$ww;RPXa;wVlVjLZpCFMgWkAy#oO zppaEnVR16sRaFzL-+~oc>M{}6bND3|GvF0fkYy-BxaNIF126n(141e@Z?1+e&>bb$ zn1KNSp{7XXKxI3i#ZEX_(#(OZudT)@ zix#%|x{y#QYMyEX$p9%_8ph#(u0tldAc-Lnz^c5tYyaeZADjH}rzh_GVC%s3jrkq* ziK%*1pJg3I_1^&~OPsK(Hx#hq02&Nqif5$2pY}wUnhgs(>-(<{SKSh(=fj1wVd(;x zM*1u$4%|Eg)KQ6ZMkIS!OwoZ=T|c51ZmU9Zj>mPTvxM}l7nYdh99=Btn>+V+?)}mB z2S3%i;{(n8*JI!HRvaTAAthbAzSxDI^i}nEw`*=gc)! zB$A@uqyT>pfL~6r8jnLrJF;kRLu2SP7X|ftYu6PM@4|WXsSeMh8{VSl5&Y?NI1Y@R~BvwmatouO$h!`X!H1T&4`~X1I ziaFr(BL<)A@O+q;c1_i4-Nuf4SGo(&{^?g{*G^5=`}G4K>%8-qn!B!+_e68wZS8wLUEi_4 z*6(7w*gtk+0Drphr$tv+&&I_of3rcqgy9ZiDM3HAwgV9bj2Q&Iq|0lyIF6cG5cDq) zP__bx=%$bGx#F&ak@(>6hX?^#WRo6K-n zeecYMTRs-9eJ`0`dN@4%hvDR5>W5CRfHUklFR4my$(=$%$GI_6oW0gZV7uCZGcpQ} zUP*Z+MAiVXNr^+Ilnr^Y?hON~$f;bTmb-5&bWmZz#?o2Vck~}SY|DC)^N|{l<29=MAYb^ zlAQz`QsfpRr<9w=7EOoq^j~95CuJRBK@OPFiJDYm-yJ1YF>iUS_}~H(SmM0015`m~ zDsc=o@nZC>CvuD^vAA2+NFtLQ20W|bL!_rxH~Z+AkRUcf>Qb3^AS z!@U`SIhCsAoDeF;ctn=}{c{3GT2_h5gK9$ZlgLHfu5dHu{ghLWU67%ug;IdY38YXh zSKZNqItOH%glJO4g1VEgP?4SB6iY18T!Nd7*pQTaJVhQ6q5Y%;Ry_WSIh=SH|3Gg$@*09BpO|UtbDT6v8C);(IzF!22*+{3-jZQQ`bTN}BgGABIpxQcML^bNX1Y$M3eNX4zf1&vo{$~BX zKV856owXfT#2@Q^E2B0s)!4Paed9e7ANaY>kNr(_u)a+{3SfA?Ca@;xrhw$xZytrR z!u&cQ6&sGk#>Dk#Y~MY7|Bp@n#NX=hJi4d8bN>&)c{DjWxo_{@J8!@JCqDU!b#F#R zU~m%Torx|*OaShLY*ONhLOIgW$~8^eD?;Snzr?4D%ZCT6PY#a1)L&k1Y~OkA!rvo5kNxpl3sB+v1mS3b@YJr}os^3;pG1dnaD$e&Na5 z_D*y5w)(-3@ja7Y{a^TVf;QwEyQ~o?xQt$Ki>SUJM|a4aIn{lyfQ>vyp%AM>tnY9B zV0iy8g=^j!>0kSu@Wo#Zr}YVJGV?Mp3?|?Rdoen#AJWKA7a#pxcvU+wBto(}61eL7 zoqnjAa_zgqyFMMReUB<3Jo-oaA^sP{AT}B_t1z{|cYZ$m4SpBS3*QPaKN-#*ehz3 zh~Py+(Mw@$$+#8)i>A50RK?Jhi8>Drp^@dODIs4i9)oS!j)Blr%Hj*$gBt$44|BJNALmz-{G8pdIU5H zvPV({up`5L2H`;291Af3idE_<^~gFHP(YQ@Pc`+FqmC;buF0m5)DXV zfQoKF-r&1p{NGp*oT*J?>y;M>Y;XdkIK?kMYpXPC??0@GpWyn#vruLTw8)CJ#eOR% zm*W$q1#-HeWK}Um!RVNYAjJYow4$8tL@o@m0jUc8YsUYytvsSkB&P~wm`s47TrEy7IQ==|yr2QF3!!lHqayCH{B&n+%v9S=U!krRTB>W4W;z zJtH@D8|^`(HL-H8zM>~!Oui5yN4sVTlFIAh*%YU#5XV5{FgExZk=*VyZ@8!Zu1_@Y z{7~bno9lCne8PMwP&{F*{6@nOGpaGnbQG%K;&5^{EbOjr-yi0;b8mO{B)@W&pvydU zQZP0NuyvA6;e2h~v`$@oDxw@@x$UxOW3Vl5i`o;78}4r0`-$+557hSGSesqc$KEah zX^0prztM2ljM&Xe?m`tDqp8{2w%yoY)fRT}E~GJvej@}Hs{oYP9k=n_+NwD>$7REA z4V~Ty(z>bATD#M_>7AW-f3kV!2XP)Xe(264Y!7Fqr^mL7Ev>ZDI@miv#|T<*p9CEY z`8I?LI#z*sZOzNOm~a1yFfkX_UhS_O8k{^fSXl|RW|*Hp{M}bhA34AK>WSI8-t!M1 z|JG-pefhDY`)0dW?`?6fI0PbAZ*(TxAABFYT7K}m_2~<>darTiM^*>3U;L;4?3!&8 zdp`ERn|#k-ZS20bHvC0+0~df+t+T6EyQ2H_;iWHp;pC~w-d(>qeZ{R|=}h?I|H!@2 z5b6f!7w?+vf;*D8A6)X}1(}wKe&FLJ1L5H0&B-9#^uF-EUkW#*4cj;WUHI~^h9gg~ z%ypaiy%>o~w34=6l&7|hZWsSFoYZF*$)jtVBt%I|&rG@c?(nX^7;gBWm_m5sbK#r6 z5srRGZE`Xv1LURC;rVZc=N=5tKgbVuKKnp;;Tz%Q$HSSI!~9-u6mcf+xshLO3LZW# z4*V`0>=k$F`+FYxcj2qQ#;@w((wqb;-FqW9%yx`lb@K&o7u|%8v+t^*FX%EhirAhZ z6w7O2X^j!CdT3Y`fr6jj$Q-&44LJg!_z51I@kSBIG52X;L2!GxxgzoC>mg^J$w<@& zRo)nAuSgA2WfREhfKLWS4I0FVM zv|X_xvt}eRC|L_SN@giRLYcN?m*&!xM#hw^T6yf6MZoFpVoq{~N|e)z0zyN{XNzap z3SQ=cI76M902);q3Q-ptqBg+RS(I|>lru4&WQkJrM~UXLCu%|&+8ih=WFKjV*6DU+ zl!Oj+Rh))3u*rHvq%$g*j*M<64H=^~!h+BPr1KU_Y70MlKO@w?JSi&{hmkT_QLzUQ0{c$>&f)tqr z@>Ibdk)z5pj;-1R=&Xy0nGDN#N=BthClCg4`imBDf;5|ayA-#J{L~^-XRPVC%k*VORgdeIAW&7jJ2CB?Nc5KupU4u;Q}C z6%qoY#JK6M2sXcAkGFO)gA>d2gYOE92SR^&aQ^H3imD6e!_tMR1AC47qy5au%><$b}lo$crYt}m38@q06-1vdo;;pr--Z{0f7>+&`9{zp0nQgHP=!kk6 z%bF7E3fa&dCT$c~u~F<5We*X;elTbhYwNn@L^60 zsjng+I;38l%x()eS3kS>`EdRycQVibZb#k;umdPV!&P^McmGAc3D$V>bK%>+8D9KW z6itFFe9;yE%A|bcN8GH!Ic3j0Ao6MWuPo2QaPlc5WByPKsZ()7)7<+9u5D-4?34Ef6y`neaY?4cH zAd~|xqY=^OzCt5q9!C{)KJtYUU7o?|O$%FfuSK$c=&`KDk+2WXyF^Zg`fx|&kwQ*R zIG_^&3FXLdRh~i|NZG2%6)T9kX5J9yROX5-o|xUW+FuMnL}+>gnk>J*u=CDA)}~ zBf;9hV#hLbMkdgW6bDEF^v8XqJ&^3jCBs8%;|u_#-N=#u6uh zh86WxV#QhPok|$cE3U8K{n7fppQ!J>hCi^LaG5LJ%!UFj3-;m-1*|w|K#pF{U8n+- z;SUBhT7$VAVds_n_5bAy*pM)ul8eF{L4kx_hE3V!g2Tp(C!OE`CW;ECTAd>|uA84F zzT&#togW$8^YO6fYX5qT@xwhvVSY#0btU_|e32XbxH8@haZW3QE-R3H(GuHY*U=ge zu{FVNMdy-+Q5JyWv6xY>Hx69ay8EN8cYVCR?}z_9+UT26u~u_jz=Y)s7IR<*A>L?| zu)OByyIQ6Ia!~dfA#3OD^b= zI#asyZ3c{Nqt=;i>^LxS;QEPeySZXG_E>oQkK^sSp)O#AXp~m9Km?P0Ky0+{^&0|N zsIHF%)SwFe#qNbIwQ%Em!w3Izxa9+G58wLz@ReT;M;?tU2mowI6j5gZkkOi#v$$Q% zF7j(d^LFv6&xdpCexrubmUV!#fo~ao_b-K;^!tavlYbJv^_$^^2jjRn*&;xT+Tn9R zZC9TjM1h>r%m;}B{tn0VHi{sB>EUqn@o?ZB{6)=^ltk!;0RO`7t1X0j-7t2?c}*2`r8qyizRL&xq`G;R1sccB=l{l*e+t1*obdK z<(J${&V+^C=dQeMtvywJwD_ih9x8wH5vT7uROz`K2X~{~SWhuBB^5Aa^|kio)P+;^ zwPn6r5@%%m7Y?=>Iy=d%t09GiN{BoG88rpZL<5!B;M8o_uDqrG-k%J2e5f|NP19hA z%UR}Dd8j`vcHdCIiUO58Ph?7_o02h3!3i@lJ=lG4Ff~`Za5kJj9eTLt^7>@)4Uq!3 z6;DR?4+Wp^v8r*C54o(d;fsgF$gT#QgIC@h-t!av+do*FS$MmRQJ9zx8l(Efv*E%S z-l8@EW5*scV?QfU)lGaM9FdiOg4h7Nm7P|+EXfops_|%`*6K74-rT(ZC+c^;UCyIX z=X}#6v@XQ*2&)MYz=ebY5|)n>tuWP2%YA**26j_OQ0hA;widYFHKuFLJ+R^V{yJV=+zfz9(v&L zb1&6S99{m#@BZ$Kzws+;zx#Jz{j-04>iMVER#xQdrWmX&bzgj<_xxkcPA7Di`K2#j z8o~@R^lnwoqf_V$-OzH)-QlxDMZn+o0sboFCz(x$hMV3UKKRSS=ZfF_o$%#fuSgY7+hfM^HvN72UQ zu`Q~FCwf;ZoMDoaAkq{#)obwDI{3*TH!w^{9v~z%VSaiqN1)?rp9unt;%r2dlXoT@ zcky7)iTSYFjbDzEhs4`$k^|*DW2s5cWXw6FKWb)0u zKyp7aAV^Q-5~N5`Zm?{(#RkMM*rHhNoELFuBMpl)W(Q= zd~Lnjq@3L3iS*oPxYb9$IP9h(_p+abN~&V4x*i_os>y_P!#@V}NfW)^&^gi!{I%$` zZ*1_}RqG||OSO*AprfS!`WAM>m?qS8!o{02>EgXJLeR=b7`JSU%>rD1(;Li>aFHzr%Sx7Vg8Uw!!LN5AyU6JPqysV5FyJA3%#;lp41!eeJ1 z`}}-;Wpc+AwFy1x8biO^d-d7nul<|Vul!#79UraFF7QiSKL5|c+G<=~ai!($lh%cx zc*W*eIS9jL=q3?lWHqO5^m|a#hX%j)o8f_9qea`w=AaOOPK4Aeb!;b)xG2}A2+^C` zHu7}wyPxMnX-*=@UC$0UmQMHeDptrZ~i*p{+f-_F8Y-Q2;Lf?=p=|$ zxMf$}W#rrThnqe;-1|NArEug>SUAA%(P=a>vix}rG0 zr=DfU(FGVHd^#c&+Ojg7s45P2Ci_p$iq9xxk&C@ITGglyDzQgcib=l>fq*kUul|wD zi3A2{@WV+@0LU>_O#{kp#kpzJZ+;~!N1}&H=)RK_3^`d%YgihBmgJQMebSLluu=27 zYtGR?esd-Yn(RY#L*l@Yl458!%fSURgW{=zL_rHDSR5ko8OgFh)LBWnjpbA@4_cfY zVI-940IZ9RRmGnaOxp5 zVx!14t@V*5X`oiIIX5glw7>%s%LH?3L&`${puwXlX7Q7m7#bkTM|FnkZSij=df~9;*KP>>M)o%RyO0T!j z8Xr%2DKj8vqaSu(AGRH=wHF5Gz8>0x!MXE;m6gHjN~hkN-PT@NT08dq#nrXJ!nXS4 zM6JInqr4`D#?)l%J@>T^?Dq9>&}_86^{v^}v)h_y7bjMaoE$uNbg?GXg>O6lQNpk_c zz}gfPFM6B|>es{pgR4$Za>Kj%-8R_c0nq%FUk_jYU&HZdBrZMBW`zdd2kXtA9_+vq z^MDzcu6}m$JhzKyj!G4H=mN!Il;VwW<(=W)PY?HskNmsvjb95#9@lDEMlp_%iL2)n zA|G=#Dz7-{{JQsq+dm#Re%QDk`lImj)BFabmkxzPp9_cnl%K17{B!(eNI3b#=k+^{ zN-sXbH>v*r+4~PDNsse96#hC-+?m;&^I~^FA`l=6f}ki;v?xmYMUj$d1qVsCd`(L} zwkVzB>to+=uCIe^{}y$RWc$e1x{__`S|&v?i;@V65hTC>A`oG*y8t%F*)(~&d-^`1 zs`~5hnb`$EfC2MNP4!!Ey{W3cQ15&_-M0V8pJ$)@eaawLoV$`;J{1B(FSjf_`vT$N zqizQ;0_+$m2^4TrYG(@`Ag0Bl!4#`t+`Di9G-pp@Sa&Xc^rhQz)a|?n(~FI#b#zm9 zz=>o>&6~ZYt*#Y>qr8v>Kr+ZYN8?Jx0D(?NOL>htb=!gqwd6DvCtaZ#y&xV8ACqZ= zd9Ae3mo$Y*Qi8(F6?05mh6=qaPeUhMP(&)MY)$c_%Tv)V()B4Bx}q*s6rp1_yg-RT zgDUXiGYfjbNKOm@?L-&x+dK-x_7GIdCy~si5}e{UiP?mN^AQ4MtJhjVQB1OvmT1PH zPQ;w-*n$Mx-mL<(1C9orKY)$~0!6J5VEDs!WEXf`_%5XjF1!Sw(d!&LNl&BVEisJ?kaJ&2O~y z!>eL?C#t?{F&kMwxA~?S+=;Z6?Lsi|0+1}EJyX}_DG4y%Zxgz_hZ80ei#EGzV<{R~ zRJh4c1#DGkCkk^j*+P@KB5&e6Zy~3Hm|_SH0WLa2OC4Q&2D1D1`Vai|)|$T+*Tl)_DOk-LV9;f~-hOmv6+|bNl?8|JuUpP3&4(BcOkHVZ*KhGRBFE z&oyJb4#ISB1vvj)02Zx;I|5QK(5mzm_THZT;9uvb@(3@VH7X6h_U|;tZfcC~D`$;= zuy;xuDO|GR`y{b@!``ce97WqoKT-643tj%!0S;0lgd|c*WRKWB%-8w7e6^86b1JK! zYE=rYiHX+yd^SJdUuujFw~B?7zHCrvX?dvPwe6aGWS8&hCkFjYj zoFC7={`FclJ9jlZb%Cetj&+6In@VHDKWgrzGC5o6L%&kA@gl}qAke=z$;h}iuxdappLk28kK88j!aV?nsDH$eH zpk-;rh=w2!ri;Pg<%pJJ4Y9yyN06jP zk2)y}XkqM()&nWAPS&gvnvxF9RBXkOXr3_%IG2$yqs!`P9)h*dA|UVDM;8hm`!m2)vwg6n zB2`wQhCRvS*JrJGK`Jymu^4Z0D1POg5y3nUQW(#5C)I317`0KM=K6 z?CWoA+&#H=d!tnK_`85TO@1fT)x0iBohX;fCX$_GP0@ZEVDCl>$uQG^r1nu-_-b{F#)8 z09_+87G);Dl4}zHK^$x4m%Xe0KSqK8sfOCD{Yb zqT!0xpOA7*R1%|6HO038D7KaZXOC+r1psTb`}Wy4{`A7w${KZY`UV%)Z7*IuU$}e@ z9kP)_590=ro2j@20vaxhpj0aCebvGb{7ma*c^;Kk?Y+mBq{`gIf3RzUw?SkwwUVB| z#3LEfDG33Mttvm~MwuZtXWr*|{|bS5{pEiCYPRWMwrWeEx;~qErcj=x%+yq?UPn6G zU&}^Umj?%n_4(Gh3kygZ5eol_uU0OvS%Z5=YjSGx_}R1fKmOETJpR<7E9b88L>?O` z>|9^kyt-Jc=w{t)mBy;2&8_0-SfRGDQ0haV&5K9oAN}u*Q=ci;MyfZxtGe$kOuD}I zKeCfwX8;%uRIwyjD$t%8FB|Cg{xx*4G7}6a+5%__FJwDjoxT2NvRA%Y!ddpzm$Pf< zv(5XneRpU3UdOT*c8a;@F8%}I_%VJ^&0Rmk zPdD9oQ?`05JywRY$^bvnba*3~2Y(>D?Z>jc`WAlUYailsLdSsPS<|ohH93AF4fsmA zVDqXB#z|-1=(?ENoK{X4a%!&MLMOVzZZ+ zHh}4W(gPQgC`42VMOx|DKmiR!6o|2?*Y+KN3n5cDY|!P$zoARI_DGKTv1YawAJJnh z_QhUmb^r|6!7sXa#^r(pkq^m8L>CaD=L*vT*RaVslsdZTLUWZj==jm=RX}aAJRtAI zLLYijf|1ZqL*%R1c35N?GS;QKi{uTjIN$?@&CUR93LH*I=RgeT**KUMCKH^v52%E}Z_i4~C095>{7YcW1N3$X z7W@Q&=n4r!TKV{tjs7~HxS^A6xMr74VIu9Y+hCrh8KZy20K~_!Y_W)G8fbU1&ENy# z;bU6z9X=Bl<1G$`CQrdloFaJPOy{P#8w^fsi-N5T3m+{OO68(H6P3AQt*HffDn*?APvV1fVK>3936(~^_|+VgriIFRSy*`nJ6) z>tCA{D%sr8LTw={SF+i;3>hQevRW7&DUFSmYJH_bjh~$}UFg3wQ#pTr_Uz#cXC6Cw z{*kAzJ#nmg@p7e792qEV8Y^vCT^#J=W&}6kBJMXkH&kxkR$N~#6s8N+b%olN*38xV zk-wNf{3o~(mN)-E?Y6&BDA%&%U(de&-?P~%?z*7u-L^-RHenp9E)?yeEC^e9$Y4U8 zmXH~@+VzF(mbdWZZ4%aQ=YJBi+@9@F0fFW&G0hp-kRAO(cJ*|aGynv*?8>@fzi%YF zDLq}h*8ZA)N0pXj@7>uwztA=^vXKuPx8D|O6RDW9c4zj~7x*x+HkjS?#%%kmn9uLN zn`?x;chBpyec#9T!M@_H+0Ads#KAv;a(LT@s0df%oPo)WxDm3|*E1%@UI?cfl#VI8nqT z1K_Y0k<8Mdj^w0k+7WuQ4jLhu=>S}SFf^YX5=&jwRTs6y2S9-1LkKKr`RD>4P3s0> zE107fip5?O4n)O(*`>f94VedU?xCWmC5;grlmsvxzX=q>F*xRY3T?ooBLvL19%@)X z0Ldm4J$BD63S?NMV6BASCd9giOmEQ?V~7OZsi5g2$G!Oi&?bV^yC=ff?YGklU`MVA z34nwhd$%GZ%n?Z@ub49s=(Y@EP%#{5C>GOfy95q?Ev#Dm7ta$e`qa}M%HD){ha%gi zA(rc3453>u8kt6iB3pD5dihI(Y*SJW(<&{H&0V%gno^g$Ia&&95F*iVXl%hcd?h%v5V;l2B!@${EVWdtAsPA1SqkXo zECm{5nDR}?r&=CGX!OG2;)O>(b46v0F<0c5^7GMRp|8KbX6xkYt&K|i6XBl|x;8*) zBC|qr*w@$B-``)W)j&JfvVfxcu7P9rppfo5^+l8WsT?4=Xq}L6E<$4|7Zz%BQN9WQ>&c`vv0Swn0O=!)jsMfyV7WIKO2-KS3vluEhd+Ig0`|0~P61 zzj#RV<0g>jBtD2dZiE9?vv0Unt`(;)7q5-W3@4T6G)EOGo?F2Q!HY<)xfFuF<|aQu zf#wS>FKj(fzx^#MYh?6tz~UO=8ih5Q2x}DJF`-W7xq>R!pPJUT0}FS&wXyZU%XW>l zSxmsztt(WB>9)*mC6c;og}GuC>0(*iVo2+)ef)Gv-Nm>yj~k|w-^h-BF`J($4Bb-b z+fytLl-8{&?buctTg|t@FbyaS4HkFr8+yg-w{Cm=fw3RBYv}v#9K2)yz=3T8dpC_7 z+`4|WvTIA_;MUTfjm6P^I4iUo+#rostruc9{Ff??E+)-TW0~LC3;xO@HGSi_j26f#m89YRbv!<=&AJ5 z`EdF)MUU}$0Jm!B(U+!|(0&9qxs^{$5s1OXn{3wrC!=%it`B+*Mv@C)cpaZ8BA?uO z2fsOEbW`91ljHmr{xARc?5sYv2Qga_k#i39|EqzCBSgD!G;w=NkWUT`C0Ov5U4?JW z6@e}ogbd`p_k|dkR!Ia(+pP9wX4nX`?v&V@U(EVSEZ_r{57r_9TfMm=j|vHRt`JW+ z+B?D6lv+Rl$4M12C^m+t#EkYeEhq;08Yy}?1Nt)o`I2-PbmYN`NhnAkIWFjh1-r1W z>qS9Jkc>w@N`fZ=0%>$MXub=*c&94Y9uC4b5dt<$qn1GwT8Liq1s<^|IJ3BR9$GsI zprzi?fS)gxpqIEnwu72Qd}?__lJsmdV8$o`9V!s;qOjoX#(^D!v@Z@pg`8;CppnRe z3q-4iO4CA9QCML3y5f^3zFOoHI%R19 zLX=Bqw5td^;^^3t%}{8}C%28G4%TUadf>3A(#=q>Y|^jgC{GUVvpg2y$UL%1J%&j>1afe4gjL#_xCmz(s#FwQ(V7HI@sHQ~$9SXdM2WCeI6(!d=`200tF z#%@sRrhpmG4?Y%x$@m~KdWgtR({kWCV4`^=_NQ+0EvYEAxwYGnD~4HE44jfBdbPFz z*f1fR8X6iJ9UWb@Y8BE&q*!@@?*U*q7!-zv0b-aKEQXAMhwjb^$zz#01khD4_f4+e zk|B*Rl{2ghR!1iW|JK7&gz{1*c$DQjT4NjP`(HP|{bv5LURvfaisgF~^5c4m*YtU~ zzM&zPD~%nmXzaghVRVDo0x#$O^BR2dEbhP!OqCR6AzT330L`Mcg46l^ubta=b8BUd z7DIcDwsVbI`86`&B$eLgvXB(Q3<%sF;5=#`yc+?$oX?}qr;7)tLl1!Qdd#p@(i=_r zI8g=L6wYvzK@ASJV&18H=Ldd$KASj~ZGBZXxIQaYv-zhB3loL@ev~4vfDmXj_?OcD zp~{9$gFCj5?c26?`|5SO*R0>WZr#2OqiYBI4<9Y}m2tYY=9{>g7b}(0`qA=^wdI>O zmv^o!_E!opGc{j-?DWFvVm7#GVf<_LBY##JySaMH&sBEb#Rp=KeLOq#DeT?2O+=i? z_fT*%XPi7pG*s{HVz%bo45Q#)R{JB!vrBtQ>vG>&%y!?Nt=qx3jIvCsz80IEWe&9j z8gtpv`@(l7(HJhEz4&9fH`^~7DaPEm!HXY2??&3G#=e?vd zqnb{0r7@cwxu5@DI6ud~SzbHO&n(4Wp;BX=$MjWxRL@gi%)b8N?5ls0jh`TsYpxkf zv%#=6fv7Mic;U8jA=`9-vp0S$yLgO|Jn;p-8P!1K{J%{dLNG-w?Sr`Qi#vYs(uABo zqzmKd0+E7bLzM%7B)U9U!;+5HaRQEtLF$=5`LP$V!43o*Ti~0JX0q%q2qP+_Luk^M zETRZ4@q=63;tIs4)FF>U<5C72 zYEje#MQAV56oH_GnPY~c!@*sCfki#-q#D3gb)@V~Rg@11sjEm4O@wOKDs;EbS=MP)XTkzXa9a8WWR!?5L;Bdm<#o~wDFb^s5 zUlz+%c0OfH zf-oHJ3|Kl4w|$|jklp>avRAz;>+5e_{YY!#zE*vr)hf2?jfHEI3s5HrSOB=?DYljMD-0%wZ`oh^06ZI#~ zuR3(PHhi#h_uC8QXBVzKQM>KuEBk*K*I6ctUwt3*B5#zD2O^{pe_WD~B_hw!h!Hu* zo7>8R>BkNbLl=woyC}Qw%y4h8L5RUPHy0?KKyTY=E1MYC=ce*zAMDt#Cv9LXTf18v z8N70i*&5Utk_zOZxZ@6fUlsvHNzy!hwB4aN7Td?pZu(kw<`6k-X4#sZ+3GFWB4qu; zS-DCrq^b4UZ0a)qVe-PWOh@zJ0HvFnCZWiUxcs5+AL1WiJRFp%D?!Hj8we{#Giou7V&g{G>GGiu`{?z6Z6+Z_X(84HSmQ=C|y-u<77JnICe49gjCXCV)km z1~^Cy0py3s68ro6ku`xYzb`K!_iHp7Gcz;O)6;Wvb4>xu1AZQflkdtik+*C<_r$>2 zqlL*!t+^SLh5B69nD;+4!Uy(9Ty`FyJTyQ$0K=zdW6zy4cfPH@X)oV$oLec=Ci1J< z%Q3()v#T}%cc$(7MFPj8F?xOg6ZzUi9_C;P+yuHf9jnu*mroz*d+?LxBM;iCPObpW zvPr@tMsMP~YK!r{ML~G! z9V*48VgfZwj$`|%A@0Cz2kI81i#Q6tQBz;O?}bRQ8CC=zyb`;gg!K~_k zgAuCV|BM>*%*U7#wg%_Z@yHRFbge$})XT$C0_l)5JZY#opPe(tNTLNG37{rly$Ost zf(ATNNw)1t1{ESffk9tVz(;sqXaIZ=o<`92n{-2MN(g}^HyW&uG4MjK!k{&DOOi4= zPYv7jR9HS7&l-XQCZr26yx6kYU_8e(Y_l!zTv5LA$m;@@Pb#qrW>JNv#Ja4Z3z4IM z-)QkBtBXHc6c+UA`AscxG|#B;!U6d%rgy+umIC+*cN1KrqmBe&kW2{3BqD*5RG}-Q z>|jepT>e^GP(T297*mLufyN}3eCBjut5?eW6(mEGHtde@7E?83*5cA6VA)nWZ4uUD zF%gZJ2^Shnn`1(?UU6uvMiGNxjFG-UKs<$=N9438$L1G-Bto=YQGLPX0h$gSHvo*HUawC~OiWHr&d<+dX!$K~B7mjnLbEY8bz$v^hf7z^EzC~0=4V^; zGp+g@(#2Mz&U4$RHxCZ{(xY7iJm_$=uUa#6_fO31x;^U~Ok3MDqEwrc;K7DQ1P3o> zSDk=n@cm)}(}IB|xS6j_CS~~Gw)@XjhG@TSL6h{;Q>$@ zFL06uM2L+$A=%)oPae5{qH$koXvQ6}W+3g%Q<5RCBuD8YfEXw%I;rDT$B>QS$GIXC zYjDAjT@kd)S6B{Icx|^)2VGhT970osMi5J8Ns#ohlcvQnq9+hWSF;CDMaeB-8^PU; zK!IZ7`6mYCibcK$m7mrS9xViFOyIBlEg|JO+gX z7~@*CYSrr1tJkbqgVABILE;7gUjRnuae3EkfFG#E=ukJCmB#G$<6o^^Jkgq+Zt?-+ zYzupv`h13rk)M^yjFD%z(#8DPa0G%iciUf?zU{9xM%JfgLhYfMvnXzGo0}&9(82eM z2~UoZ=={8y$OD}G%=f|vn+bD}pAS5IZ}s6%hf{Yk;DF+&bEmIFQTb{Eu&v|Hl|{pW z=t~5S%lTX1GPSZs&ufkPAGx>sNXHs+_5+!N``c32hhmK9Q=!m!)nA#t{Rf+)8!&;u z%Xy7D)5Uj=bM`A`?(g}QmAE0pyros~YuB>i-q>hx^9M1=Lkpl5Lc=YplD+olvfJO0 zjcm&154EO0gN!k2%(VE|SD7kS3Z+`cFM864i~!F-Fu%=Q%bEk(?tfh<=sV)!|H}QX z)4zT8#FdlJOdWsn(lcK_GdnSlNv7vQ^|d!`d+nQlbofp0s_wceo1frw#czC&U*W** zqjwpK%HTcRJIk;E3BU#sqegB6LH6>G=k$#x2uosjhnS5*pdeBRO`0i8mLNNrE3j;ux=Mr`tm0h5?*F)nF_!>MKc0_~G z8o+pDGxdUC!2!U(2C|o8QNX(7i)-7UlNZJyA{;H+-%&tJAlU)O_U+D0&%cuj)Cm+w znWu-TNB|yS`VV0W#6MWrIP2&!#M9R60GM1UyNH1yY!9CT9*Fp)3>oCBEl1N(=MYrf z1#k!|6wt4SX%}r7)pR1h3CQ@VxJL&r_{p*zk)tMnF4iKf;Wd&;s)Nrz?nH&ak4;+R z0?S1lnmZ{eX{(@ictDcrsPSUa7^g$c!y^SeKU_qa;#7p505)|DKm#<4$N30GY$@6c z@+@Mp>CyQwBPn29>zUe-hwD5!V#g}jOJ)u0g2~&KYTm_UmEi6){w-0gV2f6gXQ;=Y zFoK{g8E_OVEW3+LiW1VXO__%y$m?{X7$=fxBVmiODs{UHmZ-=V%3$jzT`dL8vZouI z^3)ZK$^qHptCvMgcwzp*7xhLyD{}Y>clcq<>{VPjux(8tHM``%L-7&Oi=2UF;Xpa- zqH)(O>7!H1;hQtU)5W2Y`OW)IZ@rcIq;@UF@-((YNTNnZM+XN7EBcJk;B;v;8V%%5 zXU?2CdGh3uBS#J$I&|#VF+YDpm-mHX_P?#b009ipKKAi zCh31uTpk!#yU6`9$n4M|O^Zls& zpOI#Lq1kB3n2tZqm?n;GTwF)jO}_c(=Qi!l%KY>lFP4dAU>PrN9-fUv=W76-K{J3C z85k{U3=i-WrP@TI6U<3z#xh7UnBmIlBSWA0uif9-kOKzI8HlHBd-mh}cOYEb+D{q{ ztkDG4XycxhHA4NI*XYcVq0j!;g4QTM-)VXS+}E`t%R(`RRcwuJnEkNho^0^Y_Mx7g}fjBs=!!AO0^-J^9e3W|Ma` zjVSA1vuSk4!Q0>Vj#s?phX+Q-_&KOgeKz~%C$jNpxgSm2<#Y%^2OOlS-fbHgDL;6_ ziH%}Tr#omQX4upsHVF95fe^zmQPZltc<-xL@aU-k4+TtcsD&+c?6M?%iM9;7ESco< z^9clB{!mSY&7G)>0Jei)H$4y?80{__3$(IdjL0qm)5;N^3RHHDI>B+~vmlO0bL+W6 z@faOLtinsmK++f?>2*P?9a7TD0UU^>(HT%lieM&P6n5o$9hH~W0Yn>YBCsH=W)PMH zCdcLtc0pG&2}yJ@Br&Z5C9YIVq72Bm0FEcoOcZ?_Mj7jcc*OSr1$NMK?I9#}fWnZB z!feWpYXda201rnL2{2J$6Eutaq-aTV>@IK9wZ#+iO)6S9{04wbmKiWuXhd_#9i!4E zh~o{N9e_r%N_i19D}tuNE5Wd3)5j+{_%)#sp-Z4uy2S+^w0}YeexQJoPhm*1!Pb_ zUl?ng2~_kbHFU+FkS^v=1rw<4GmVJ;^df?Fk_@#t)RHGR$<|8}zzc240Af}!cGZnw zO*%=!S6??_2Q4uc!BYHgMALez%l8(Ce_Z63XEJu|#H?O=>$Xk<_W z0|WjS2^VEGL$2C~e4cd2b|FCb&k zuja?Ziwew1A`H*fGO)}tsR+9Ab}U((ygc~L&(yy92~H_T7Jn0X4d9s5xYK24DL|4c z?U^dfy!wYHUis#gHL}ceUZW|lQT1zo%0s>r7^hhru&8u_S!N27ilX*F3iZ2wbmrE# ztgMk`IzJipj;7MZGWU_N$TJ{LbAlPD(aZm;s_UU52ima|;Bd;lF)i`~RL&jdW5(fi z*}&GUy1P}ZX4zz+&@2=ig(A*LTy0?iLDgH+Q`yW^Hh-;EsJEsLXQw~ZIQhBOiIZRd z+SREk1dR<-wSUdVkuAFq{>ab1;TL{o?_F=GRg2lVr?Q7Wls)v3?DAQDeIGVzj3IJgF$8X}wKYW3gM}T+=wtWU7I;?{-H*N9* zPKUx6DNDc_95HQK%rmWNA`!au(iYQBU2K{{)Lz{LOLhSL2wgQ`0=1`i5&7o|nuQs~ z%}W1`st1hx*oJbM&loGU%DT-n8+K3O#F8b;Lb{JDr zQx`8@eEjjpKl;&+e)z*5{`}`Z|LCKSo;!DLVqyY`VxxiEB#onnk-QgRQD(w2yi)>o~CV|(WSgf2SY z1iE^$>jf|_e&lO}2b-4!7|e^0;7o7~j?h3e$n{KP9@iBB7D|<7wZHbvL;M)|pb-Jz zf#RH_Q>VENnH!K=L7vyBpbVllu9FGg=qVr9ltJh=y9$2H!bn!JU?pe`?{WLbCT>yi3Z{5Ig z*{b{9r=c?-o(&|hcd%oag4R4T#zR?ijvxNnzrN7FuTb98;_hk&F>9BF)X|^XQd~HC zeBttAtxKO@IQ`lAqfazWoh=tz-#pZuFOF7Mty{hOmRG*x=im6Z|KTg&^wyEF(c<)Y zcH~Rh{qM`3{9HCatq2vZ#>fEtigD>?9u667O#PjTK$IdtV>|9Qd_vOs1B*A=-mzIm z|9ORxradtV)Nu#IZr(S1aL`r4&Qb)61CTVDwC@x#W-l})YX)LYk3%s8mNXVS%{>8Q zm$Y01K_#^v4=0(5WV=@vJ$5Mo*aZ(Zkt{hmLDG+C`!q=nwp9(P*?;yX~C;s(#hfu*ExXUQ4AZR@_P=SO19Z80M6_uIjc6byH`ccw!j3>0!OC#}M zX!BLmM*O=8T%g&WeEZUfFclshmJEnC2r#&0Qluk$0Ys25d3K4gBwoO=Oi~9_5t5W( zU{Tz)sURNi6Cp+9dk-Bsd)tuk0&_*a`vi0lqj~OCZ50LgI~4t=1|c~D6G0h?@jvGD z^$k=Hy-~^>pQjQRpWO5zsI;1e?+Au4C4wtWo_f_8UqtYVrj-FG0*)GanE*}NlV>=9G6apT7R z_NTiq0?eEL0TOw#|6uQ94oGwtXQ5HAOI-A za}N~@Gy7gMwd1y|HsFAPt`#)$Owg(6T_bk1h!!J4jmW)^M6aUz4gx|#UdaROOgiv3LU=ONV7}WGcboxp3O$8nvh0;X#KZ6(=FIJPDcy1LF1` zU`5rk6b#Z!-06Clh|pS)M3*O!&&|LR9~^$9O^K2TLB&PiPw4a`aHMK+(5^jF4mmDj zesUO|OgRjL(-;dT98E|;9Z5UpiW<`LG%^guU}1DYo@=DZWK^1AtS_=MG#RtJyDJGm zD@A3n%N}F;2cj2Hha40cuvZ_hm6EZbS9b6#l7LMIkK#wxuna}i=weh5?F+b6_IGm^SIUjPeMKcelh1<6g1=!0dS0VkOO%N2EN>cc+zz*JcgqS)1^1S1L5+b z*x(N8h=GS$JSG7TlZNFT&2%D&N17As{9eKyEJ~D8ozztT%BYLMnE` z6>*`Ae0jYPvpw1ZU?UI!XvD!7vS!J2B(o`23W+61sv~EBAdehr^Ven+6lu&>OzoI4 zQbm#iB`HibJRIE&hM@T^y_#54+h#-ktqcBqjr07>E>+qz1 zwW79^gUx^(xhR75#j$K>7vizU5cCHZt`^pw=_2+8Ay-6Q>uYS-b!}kHf{qNyFAi)V zYPA~jqOq|tY$?F8{Vp^bjq~Tvf9g}8`ZxdP-`soey(dnbKyu}M9(rZz+lJ)>xbg5f zPNCUso;!E$YhU}?7ryWX%nxP;`$7<*I|pcCH--tsR?Pp$7!JFLUK+nc&6)m*bKE;Y zUKt}dVA`|!;KwREZd5!z3WaO;{ME*)wYl@0KuD-17--i3MpeGCwuThmLxs;rBJ$v3R5 z(Fd{TU@req0DQO_&A}u_V&^7K+nv{ORV%x|KCT(`(fEzl(oknmZW7 zn>G|~dax~xvrZ0DN^vj+kSCwFqCkKHT_;rD;pKrz!)C5;INN;>|CMq5-mE&v>=C*P zQ?2Huta&*@-I`cvF0?K$WD^ixmkkURhu0Mb*A+@tUN`Hr{NDNVhqK4;&7OWBo4!oT z2n_Dp1&(-HhQJaSp`HyQLX5&gKtZG^7%U0ZNtkY^pm$%VC;C@QcpOw}g9G^|dJ)PcMVIkIVh3HiK7 z$AFs5TnAO8W~V$J*b<@uttlaBNA}nVcTB;A?MI3Ss~QxR1>fFPS;2u*E5VB(IZLMQNJ^V} zwjo#ry-Aj|el;96Y1w~_;V%&`!`4-8%j8EHAdnh@7e~S@)Mb+*vY%SrFi&q*A(Bj- ziX7avm=PeyPGpaT011T7xCZ{lMSspswN~WEqV^a22TJ|J^S6B8$^P}&B73{T7a>mr z{r&xZu86dUHW$Oiix(e!@WD@i`qPg-`sn!hc)eadj|<0XXuPBt^S`lG*Dx_FBD`BDg%vGgJ=KYPVebM4lPKf1C;#0y`eH6MR(@#3jg zv!VImLim-d;HzM~{NS~B%G%c^Smc=`MzOT8cI)`t-&0swqud&GK3#m}9D2CX!M=_8 z-U9zSksCE{U$hGc6SqxlD6z@&&MakIAkd^%=Oaokf(XcP8w#0u{ww2=`?52SXU+Mn zT;snrwn~NKszP~Vp}MP3J5cO9P#is2T))3KwzW94u28NOpfxv@oq8<$=AUI>{Lk5g zf5NZIK$VhKE1mR623SvetBAnNUxVFRAv6X84vR0GoYSJ=Taa3q> zqHxET5!BW}#Z7QjH0|E7-ItAMCTIcRz^1)Gl4H@lq2HrJz%-~)SRRmJmaxSKB`x;? z>;+#-(*?@}JJO|LizVSmpbLKec+lhJYQ~ARaa6Vgo80{?5{O|2sL~TN#WcFE0(h9H zy>F&{%>ZhU_yq^!^&v+?c4~t(Kv3MX5w#FTC|V{E06g;Gk-UD%N3MuYu824Qn&fNL zK#10advj9(B<}!+pgH7-B;%4iCOeG8L}XIa@+PH4%RYePa&!S4^Fy89Ht7<>GiWCk zMkrqf0&qx4-Er^`b%gT85;REAf;b#Y&K+tFO#~dd_-cde446h(bO~Dv%>eO3w3x{h zRWt}mb5Q6SUGYq(v?>-wXc@<2kCu^=f{!tPMh}dnvn+^7SFJ$UDTQOPFAD&mVMh$s zCJZ;J;)Xz7(X>x@ysycvVOdL)sgBSaJv#(XSQlujZH{s++>23|D+Z9QsQggThcIm= zbFwc%ACd&`p_J^(6;-Tg9W^B27fFVA2tDm|fD{gys35Jpti(baVm4`YpKd}lU`NS- zgjKsOF%F@`f4O8UDn^=uJV5x<5k?*mbP2S6-HhxUma-DCjFSY*V8mu}Em(2c(xb(4 zfuDB6&qpov4NkA!F;Q;6=W0<@tJU@E*CPumFZNG*7(I4-XU?4Y^FROd_r33ZPdxF& zl`B`cpR@b#bzqnn?BwJmW&mTy#u2F_HjcepMl{je*VhLcJ233Scnk{Q1S-9$(^b9<6e9?&del zZrr!BMgRta!M>0+D$h@s&ppHQ0{t#wc2ore0gKO?Et@vV)%jO`-`u8^H6kz&3>ItD z^~tDL)vFt(uSDD&YjfkKRwC?mJi8#k@x0(F<|m5ivM_2iDJB&viZEzRM0GG*voqUt zAlr0vwt9OuxLOHhi2{w;Y;GdEbSgXZRCeYN-^)648Q037Y5VqwUwTvzcWKx=#=WoM z#}`OtA#YwjDhKSHF=}vVBo}Rhy+9s-0K}-MU4vb7#!O2crmW>Stq9l!T}v0ew5DJX z077+09HUGz=~B9DJ}tP`8lgZeBx#&3i zScOng^X~=9$&=YGbjjNhi-ruF+HZ!bgF6cwN!PF<;k{ta7x>`NAR4mPF$!_ga#J@N z1wZ{gDi5kyD<0LY{vXQ5NE`8I`6w$v}${s!jFi6k6nIy!Isn z3LqbbWB_nD6|(QKTg!gaF=KhxDWt%UUHWB(rL+t^=9~f+PQ%AA!ym|G5P%|%*tQEX ztwoMX0$6oSX5)WFa*kze`Q39X{!nK%Y^F^le&e_EqDVQB23MzuU1uIn1n?8|B zU~REA(xXL->z1d%A#3uv)L}CkwJV1qf&nDhh0?sXa<1P3l8JQ!N%KWhU#BOHiOF8J z@tZiTZDs+|`u8rf{o+f7nv}6=DuoFKAi@#hSLv=**wtm97H|}ZwC2*t_e-7sdlT9Q zxq<9LWc#&CsMAP*WH?89;XwSJ;1WL*l}{J@3e~<+Uw^T0pf$GP^6on?_peENAQM2M zks@x}whbu|HVv}bje5QQ%rnpY?(hEY0}niaEGTeauITUYzw55M{_fxXySsMnT4u(^ z$Hz~cID!2!AD6@jCPt#y%v{`Z^5N38bFJ`Q8vORf7E;GXy~PiY;GcGUEaK>G)&|c1 z$UmGN*;t4lqyb>(8mJy%S+T(#9v;R{7fl`~*8`%xa^=dobLTEyy5uK9a2@(yd>}%E zihx(IUcF(%25ikSgwVG%Jedd;0bjj(HBERg^3SgpnvKe((_Ev*aeQCrpJq;FoRv zMgnUzboCv-HaEJmMja^Z*|Uf9)oS6QcIX#IcyME+jvL!A@4yTYp*IS!sctlRty1mY zP>V2}xg~4hchG8Bz+O;8ogcocPn2OWupoH{0WG?-FEH&(8#4I9hBfW&D5;HIlBb|v z^1jj#(9DNwcL^q`r5Bow96pk=!Ptu$@-(j6-ofN>&?~uMTO0^5-BRL|kjBFJ1CLE3 zM60$3h{2kVVvnx7fXQ?iR2L0UJ*J%?zzg|RB)#g+YahiovjW$XZGW&~kKV$>aBPZ1XxoKf533wC|0niIE!@Yo+EQ%SL zjr2V_p7%gM!xqWGGD4cpybJx-bzr zVT_(bMGM_jn`3}*w629F_nDNU4bGT;1T7|&dAU~uLg))-km1264sGU&Xh$d(+wfyO zRYZuTrHd0mCd{~xuG+yeqO`z@A|Q~AnfqPp<$3|OXeaPPl}06SOJHC6wPctuCzttO ze_X14nP5#?0cZ#sHOHhm1kfc})mG4NtBD`a=XGFRLrE++xk1oeAiI!bFD(?13`#64 zmDUEE7`_u#|FT%F^7}KYHROsY3v0KH@4Wq5ZNyt24m!d#HWnKHbKl>SJ3@{>VSf$`x#G zy@J3}+g#+dV`F1OLqkg~YXbJQM~)mhEPz1FY0Li7uXQ>;F)kdFd-v|$CxF1NHwY~T zPx$2I)F{4LVhX&=!4&(p$r;zYV33rT3Td*q5G-7VAPhU+ z&&fsy+;p!&;|haGOWcrwW4=w;zMZm&0AL3jR_tHsQQJFj?oliNxCuBa%7}yFanTD9 zUWlX%fNh!JH^)gDW|D}p*#HQ7^n`GeMZROR7&qm)J!skrTZSCv%t1>-@Iwy(0v2;H z_;r$;tU07mI|jAEaRDIA7-R~)kZ}TlLK(@KFb855MvYt%HDk7<4T32#fTqlhtB@iL zE@Gwu*a(~Eq;WwvE*aW%6t$9H6Bg$2AmikV2}#J)dkiCOWmBEE!`OPX>@hjDWoNKp zrb*+VBoO3PL*2&p8^Erd47F5j!F;%;27d`lCP(NcQ#FySwOJufAbG*T<97?^g;_}e zfLV@9bB=LAC3&RNI453Q*yrSUigI4S2TXV}i6p7EnXt(qb8!aUcrfH)gO&_pvIDUY zfVzO-!pzRzq?kui^!)e~v#+)V7m4N3#j(8Y+ZPc7aIaO;*v&x=FCo+@p9hV_;RD=a zGU*~up`z9cdc>EJaC3|@K@|n(qW?}MDY5_xa0$c~!%hQQ{Duq^9XqFr$H+UQB@9{s9=gUR1SvqgtFZI+c(*#N=5Ldc_MgJ;&3F`-_ll7ZtX;j5IJR z;x5lgv!%t++x4R5ia^dLz&U~n_5~VANEfU8gd60F`o_g#|3G8?&hc%xPS*I97(m)a z0HBd8u3o)*+qP}k41p6YM_{9K@#4h~edt4f_Gf=~;lhQLTrmMthm3LEx^-`R+uPpx z&Uda|yLQpp!{+$Ji4&JDUGn1vZ5@e356@lOa`v13mrfvEY|SBKM7lWFs?TH3=5!HO z_(vivbZxM4;C0X5{&p>0H@w+JR3HGqYi?j*V40^1Q&Uq9J@nAmh4JxmL^wM;J3T#( zn1jT(8#pF0gg$*B$C@>3?i6mn{r17Z!64CthQ1I58~3YMuVS+fc&gLJy$hSZ`X`0Q zzto!H1IEs?YYFJG5eXg#q$3LbgRPrifAWrZcu~5rvPQ;pU85~u|8U`nutpuwWwzr1 zBS;o=)B)2tdw6$b1HXm-q3ji}KmF==dQrOYa$chXvPn%>3!{DV7e~1-%t)3>vXw446cu z0aZk@VB?_BOub3h9MCX=2AWC51x<>{DVoTZv6`TL_$AkQU#5W+mBAt8dI5C8f+NZE z6U?!9BgnWJ4JJ@hHQ+ALJQ3sqNM7vG;Gi@va6KZHoE;Q)iAheHA{n6zchP9jO0bvY z7eGH`+9f&yEQZ>_vk@a_9cjKu21ggf++_#$(zX*hTnk7A{Q#M7FQp}cKtv;MT9>ri zK}Hq~3%6ZZU4_FDk72}7Sl%PGfMw8~1OU)1_?3+RMj1G)LtMOwvVt=8C=_L+sI^iK z1Ipb=jyDIkTnrj!umv7IN*BQly_6?JnuV^AD}qu-cr{WNok!Ryt+7^DG#ln1#7`*x zoIwX1ec=p@e2&iyvn;@A(=-EqI@-<^#Y81Zw#48I?Q6o5j0L*T#X*x_5TX$TSfW=` zh-|zrA<+StO_sy)hU`ylty8&NDCq-HnJ)63sHOhFxedFRhKi@gPK5W&hRquGmJAVAfe|)L8@=i`p z9zT8@n`7*fvG>J6|K~pUxsxYPA~44my1!H5V-?uCb?ckn z^rpM+x@-OV^^0!eF~c)6Gf2?^&pEJEDzfhQH!5HJJT`yN zPoCTMDz8NsR@NxtIjzz96AxAI`(SJ0l4q)Yq2zVGGhGDGBExqPdZI$Oj!R+H>iQdg z;=+zQycS(}Ij>O(moN)q{D1!yRZ`wQ;=Zbj5GTcGRj6h5~2yLZoCv6{Pp99S2qTuF$mOnq!L85>aBIFJyfM&HXw^PqpLvON$3!4SU zic;wIUMppNrQjVRAi}%jpa>&kh5$rE*On;k2(_K}13-5%Dn*V0!n)+gVj%KhSuBe& z)R+pwCcVp1>I%n?IQ+k_I7K#s@HkQoEgCn6(dH%@Ov8$9qJmjp0%Sv4QPCsiaOBF- zN-wxJs0ELje-!peg^D7`ml$Xp@p$Pvh6=vr!QrgxTRQx(*R$#_&qLc_=WGKAgECPd z$i(?Xv#Yi$1CwM2)C|5XE!ZiF`S67x8L9@`ji`u~t?NHgp&0&oFK5RwgXUl*PrWz_ zcR{NX2iqO0ISRl8ptK!A|o>1tESO$(Ln7(y5q%js@Pm zl;C@$_8UsD7lhl0G?*NPmoUZ?B&8loG?c;sY~Nk1#6*?Cb2AknnBj9IW(5J_v5QAI z8SbGB`%-_pp-M$Erqu)meT2g~K&~Ue2@*yBrB$ki;($hohse#A*w_e&W7|IW@Ip>R zj3ZQ03qra_Xe?4vVTUMLA{hz2vPA)c15uMTEyV-GwXB9*F*MZ0A}0n(7lqonN|Gt~7>MPp#U}DDjIix-c*>uzB-l zY#oBh9I#76u82hO{qKK25+3a2g3bzZz%0$p%^f;)2)oRkJ9lC}!3;AP;jbi_nwm<_ zW_d_QlouMqQx|JfS6bL;HX9520>)Mo=^;FH@Wkd5zXAV?$_*pXuhM~)o%<~P5I z=E%s%(9jTi!w3a5F%Ot_s9<&c@M-D~8A1TaU=ie9MK~VrCbS8FBFVGrTb@`^( zH&)i@Te?P#`h5SfN7KKUc23#i3qkm@%5z6V5G!N@!&eX7v$96LYt)%8-d5v`VeYWI z)wL)TPPpzp-Zr^}t#h1YT(Qhdz)hF?=Jb(_V8v{sX*7pSz_J=j?J>yMsPDJF;{eE7-CS3 zCgeE;X#q4C$j}8KHb?^4`p;`x$W&n1$2+~TfK8#K)d#!;kWa9T!}G(#wo@>3M-Bk* zPBUPHBUQ{0;LtpFyH?>`>6qaFZ8Q;pEYhPQqzbxhM+5gfw?l=5Xuev;dqahXqk_+z zH5=}74d?7tknBJa=chQ$zxar_Ix3UJ%RBnNS3)wW#%O}jT2{9D(&_dgA;Jf7c z-H_8DiQfo&L~Q6y!Fb(MoIqIpxOR|auBgBu=_lKc+;*_;0Eet-jT8VV@S%tak>KKB zSL|jaFr`B!I8rg}ltpKdk2Ju(P^S~Qtp!4_egTuA`f>zo;&=v7v&6Ntm}`I@)i zE?l_q!4H1$kN^0Ov6=8T2+UM3imonN$#G2QIm;jk=jGi5LR;u+zSvka0Bm-jdg>_< z$QT!W4<2@(nDDDtuVN;vhdIb5D}P>rm;xc`kU ztZ5It$HnFw`@Qw+*Vpu2YF)qv@qq^(@cW9tdq3C~0VC{d*REl+`}EUKpFMjP%u#(< zrAFBX4sS3a>}&)a&388zilvcfztNiV$3QLyo?iOp9J|qtbXXKe*IhaI{Vkk*Jy=MK{)XpuGQOc!sdalZ05J!p9^ z=nbNVP2~T6>usE~9ka2C;1fJ=w$uyv+G}tZwf+n&=e&7!G5 zar`N;Jc6($pYAmCthJYNRGeYtFbX3GL{MF`WF}QC1}GAn<+ON!UMcPSg2y1+Rs;>c zwKyS)oKuv#QXM7Tq^?F9rbsMSQ6>SC%$ZbF04fdyMZEwxE)3PjK3Uej7QbNo7%Z)Ff0xm;vJ$a3fR- z42PV~Yo6LM0BTu+nylJ>)kJMw0A?`|ba{+mqL->sg{8}ag=4O0m>to^G!L0d)$SIutw48gp1 zEE*VZ^c(}!REduYcH0)f;i!WTdd}J5t7uOQ<@BQCQ4vOvFGw~p0kuW&#WS7jj%XVS zWR9^of>qTy*o~r$>0$|mS)#vvk>90Js}GJ%53g%>ZjX>6u3fu!!-fsm5}|2(21bf) z1QNxMee7eClaqmKN3mWMU0rDAIi~ZRWsrpP@@@j5Ep#BsuC{(124P>7zJkWhZvWe2d>doW3UlWeqUU{TfT;mOnz;FvvNc;S@4ahfjSLLo zTeU{hN545VJzn4$L8PV9CMIWa@k?p$E>bFIYd2rHX=RN9Fod*5o$2DO)%L7mhOuqM zb_gel?uTT0lhmGl&9b|6x^Sz6Bm_AIU@h>umDm0$)l3C-hH-bOys@EojV4oeVdK0k zg7O)eibX_Q3|qKqZw zY7$@*+3$2Cn{ALrzKTd7=A0J7+`?+d2|Fm9UKKi(V+b(9I7}3GrKlW$nFtuhIb&i1 z2n?Hk#0GWFgGS0ZkxhW(jq*ty5v55pAOJqc=v>h&V)Y2nC=4F8LIPdFBxoMwM@5RN z0?_NjfX1ZGSJjM1ITaH{<%+=tI|JxN%LmIa3zYY~02L{lQkCWVpSnJg0*^P#~q5ud6 zrZ_ouq74YA=?$UAr^Mh{$^+!#C&DEm%!Y%IN?XQ$AtPFp1|naUv3CcMM;C+I_)UoO zpXksTPyZAT8c7kaA+SzZ9b)KecZ$mS(M4qw;mH7VMV+b0CEDKnLKT_J zK@-x!qEWuK2cZR_t7t7RPci+fC5a%WiI2u3k6`efPBjFMB~p>AS5OjWOE)FL2cR8$ zHiE$*YF*SU5Wwy}JmwM9ft>|uSr9jewrh69){^Ma?}jZbW=D}C7It#?<8)t77fZ+$ z`4OpwN`+4sJJUsM2KxK^ckI|PFfahx#te)TSi{X;i+ph8mCX6 zMiRbr=T6LO;sc<)6ZKaJ@Mwxay10r@7cc9}Ga7zND(_;PdGC36gz($^)^5MH^_6ni z4OqrbZ{x;|z0X6i3xD#-C;#Y={^%2*_{7PRC*d9PUUUc91@KYlyON~4jAS9->!y1X zCr>Pe8x-g=jv5IDcI}4`AHIC~GG>0X=L-XzjbPNXv$IQNBU$y-Q>AMciVOTAcshe^ zpZtI>8f@q{2cNcqrxnT;Dz*7_JEpeZO2r%Ts@CS$@0{Lo3nT8q%Y2P`o-P6qNQt61+0fxG6K4huMQG9~ti1zzont`Q`=p?y ze-sS0j364wyU>Lm^)Leoc4-u5(G~VvcEhOonrbs0A%v4n6y!NhicC;gW>O6qM8aMN zDQoDmN81Ff*o)|#DlEB;c!;6Mr+G*er|OjSvrUNke<{Inc%bM}siT7d$2MUE0w^Go zJCB-lB1H1Wfv_Wi+D3RQSMFDfrqbkFH-TDO0tTi;(UOp(Wo2urMi`7e2BvZ7e>j6N zw!PMv5QxDn;E*s0fsh5r*bz-h7!)cvw-=!+Gy>%&%zAXerkDT>vxoyp1{U<&fdLe7Bx>3=1_gP5Yp0IX%Q^;=nx1oj z!m$rC$@$R%PuG(0kRj!GP(ZUp2kceMc~g{BKMjhu7Jx3VeitlYU#U>4M-q%+(9d`k zvxlFAZJ&XuP(@J%hz{d*iKjV`v;g;_hk-dJIRO(Z$m2Zfq68U9B^pTHlMA3FI!S`C zFBCN4J*jsfXTeA|sl}HlV|T7?DqlxM)Sf-b1|-j9Ca%Fghvr%$l0m!0QK6Oophy4g zBaOQUOKzqUIR4}`-oiT&Jy1-2u|L_0o8(c4i#dUk2$QOmeh?Ahz z!w!JCVwn#Z6ZM;|jBP zS(Y*uft=b_|C! z&^7aD;(*7TBOB4V+>P4#XUmt*6dQ9qhr$UZKv`7}pww~tYK5|eYJYvxo|(=2t)0ip z8oj_ZI(PiphmM_lW}?2Mwoof-!{7@er{1;UOaGE+)%xoj_s(p&DTI~7${KY&U952< zh#LeZH0{#7GiIWw6GKZNzBkR<9wowx=;lr?rik8?!2)X;0Vt^~5e4TKi#ah0?JMmq$ zAe?hjAj5(IKCvZ?;1gQ~J%;A&3+`f$7EWjy(X(h2tXi2(L z-GT>Y%Q>F}!^kmZUd6)E3!3H_v?hLnjc~RliQ<@1w*}&~2X``JSUt$Vj&inkkuO;D zkzg&w{oaIu2^n+RL-1)~h!{^TMLhm;4#jIE-F_nBUePm*p)6~C$MVOsvSFaRP_-owv=$< z#EIYeo!>#u(^$zBpF6PAotc?Ab?Vf?g9pdP#=@LNKmdE4U8kiwZBw?81%602TxC%|x5X1DVFfix;1G;)yK+rX_R;NYs%GAer|+;+pCC z2dWp2RmV@3>a#kJ!o|geCA+~j0YpTwRqdbMa$tV_E*V%^qi(#AHM(~0^yy>Io|%~W z+B82BWk=OJGax#ODvfI2+?own*KWVMZpY>IJ4P;^G~Z~|2I?F4%x>J{XthKuYZTC# zF1|`n7cs4#SZ;u{QS*%B)mtL()v(!OQ%v-?EyoiHijYL@W)gsLCW_8Olss@DEfDvS zW|AH4*r{t9!R^+;pblo!7dFTmI6(03t)ZGh#{XQTbOMpo7qWS{PFfmoK5P)yqC43n zGYU9);BaJtDT-7ez>(^Gw$QEme;2JKPqqUsXJzQZMivPkfPNPuy0dGxoX%;R>*+!h zd@h*==)?xXHUfh2%MNp}I9S_<3KhL%a^Zh>bW|P=g0ck?Qc3ElZ7s>jk04$AI!+V- z_Jcds;IZg<2t@`_(CdLPDkOxU;+pJCJA!#1%A__4{Gw@0y;*b^#O%@422|lAX>mVr^&N&(i zl?Q_e58v{#NKhaZiP4UVXD`sPX~2}{GwGs_wW9J7)$S5Zz?5s-1Tb~n?tqC^(1}UO zI2&9VN+}Y|t&R}zJ$AsWV5W;lfs!A!)hQqXzg7=_L-Ak5l2$x26SzzfFp_`+BQFTr zF(4$01!v@kBI8y`Ou~WXEO#M@2^~^$VUL&!6x-DC*bTctQ4GGbDo}o7WfzKkOtE(W)2O({T;A@MIlGf4Q7``lF^{r&4fjnIdB0KE(Mh< z>VFrRG5XP>N>{qr*Vngk<3?-@uqCjWg!AXmf8rCLxcAUJ6+U2F7hp@{LBv|ip&*pBr0P>y4W|fanH=!9rofqE`Z%F z_PDE8ueOUk?!W*3-}sH+ICJJq^X2iWw#d~dCMF(y@WI`?cW>LaEzClM-(HAR1PKWU zPrgyPc%pXU*dX7?F05MPcvjY^7tec*#?PFb9zQpWzPb71^W_us z#ajn)M+{d;q=@tV!;`BwU*5F)(w6;K`o}IWWG63OIXQl%v1!{-t6r(k(E-n+fw|3l z=hyFaJS%IIN6*v6beQW#6J{ItoAjnl)ZDvU2nZ5XO#3OkTq7|V_Z^!3|1Dw%qAhF+-iFwlk@Ebtwq;{=cMCJRQ2A7L;< zkST}C3I_Qa;^$uwAfOu4!sXENVbQ%_mQRGEMahE#^6O%;prQ!@%X9*w1)tX9)B&E= zP{-k;Fiy>hV@@tmK#+r9FxAv=H>jxMFf4+a1$Goqo@tI@`|{8hqLvCe(B&D1SRhnr zD*8$?rU6@^0Oe#;uk2E7m81nEb#}Q|9Lj?g)x!A#Mywf_js&#RiRlRe#|NRcEnmVZ zqw&)he#H#qHRPHWQXM*IfeR`ksaqGt!|}LM{6@0Ikj!z3ml_lzH#8VbV+1Ti7cx$H z{MuH8Zz|Gcf6~udD0JoMP=+T>`XnfVLy40r%Acx>T5TY9*66!!=baeTK79tNp+qtCBR8hlF8Pv3IIkyxxQ68cv(DMELN)e zsGGGZUBs4P%a$$PndN~5=1X7t(jWcNAAJwM+v=qO_Qa=8pWeE4tN)2H%&&mna=AP` zJ&lY}2N=@D#;WOyeY01y#=JfgmFZ%k)hseyRK}>kJ+!L*)9ZH4cBYFkg{54#Ze5ob z1n{-5eeIwB^M8Kn(j~aC%F6*5?A+WO#=dXgKJ5O%2{R(#n1{f%Yu7M8ocf5$h2z!p z&y;R3UF4-CoacF58{K?n&uf|k!{w264rXPI76k_TJl5#qsT1dpJzFj`Yn2Kz#!L0= zv6=Fk!M^I6b>o}%BSoBAv*}`?a`wvP+4JYmj$b)`?#kuq`RVzE(^qCT?AcwNo37OP zU~r+5B5=a83X}7#~Zz)7cOQojox-^3xj|w&a5;>%i9k0JaBmN14Hlu+w%^XGAc00 z*fcYa4Z;jeW8oKX$Q0e?f&|cO*TJlh1eDx7v&OuE&4IhJZd{dF^30CJOBVs z4*a5tUhvH6_JpXcrTM49C=zC}Q{H2@42yy!N0HE@#6qtEc1R=?q340|M0L;{f;H6y z*XNN1P>d)-LN6aw4`9CN0K0DA*DK6cSa(!L3Gz8`O+ZK?EamGOMEH8U9A9Nv3LU0EYT`harg{k})MJh(F~g;IK<+q`lKfFhBvDH(@$~7IG{YnNhBY$PL5>M-I}$cIqy~ zk4y-lH?tp88*3@}V-xyU?=37{Q7Oc(%|LI993 z$7_dzPK1#ZgX=gya1?w*gh_xL6k#)l0o^?0Di{Gkcy;V6@qxe>D;&?UPVt~nOHp}} z?ag(X(ojbghh&<61JbC`4PTfIc6svcMF$DPlQo8pB#VOrN!yf}Ho^% zY8p`i9hkYWlLea35rAU}#loxzG|fps7Ye-in1mg=i;4!dR68EXM2?an)*J>FjSY43 z6#~J0iWFs0pf~inmbUIBQB&RodQJ$w#0q@ffpP}hj-8#k5S^_~P=T5>6ohh~Y1HNw zfO;c1e<2n0^t3fKAAA8nbO|y-#l$gWHqbYM8y7}Gh*Cu1$rO_L_Nbme7SS*2zZQpw zhkJ7s;EP}U;^D)G>-BnAw-vkqV6TbE#FU1~%>k2-CBQ;-J<1?>T((yp1WP>E^Tz~^ z(t&{iq@Ab(OTb3_5a}r(@3}sm( z7&wNHNx)Qq4pSqfNo$|;fG?VXbDdKc-SH|y)GPhS6)&ydJ#lUPv1cz}p+d*X8ZB$& zxvr5X7M%KgV`i>CP^;|OxMpOq`pDrEe{uigPo2JUX{uhwW&rm_pDQ|DNVQs*uTC8~ zcH-Q=J72nNA%@Q$47(Wz~inSdtbU6_o)Z|n^N z3@zG$m+3k`Y84q5nvkTrrV9k*9Ve&JU7iQ8j;tl7CxKa6;wV?-5abKL4%oGWf(!&) zTb;wlOva^m%*=}Q@Q|Yxhx4OEn!<6(OmM|Da5!qfpuA8^P8}P=_AtdP$gCyPK&YjH z5g|ont}c}@I*BnJ8Aq`+Dg#6qR16O?oSbw7f-eg;g)ln)j$ng{lw}Wux|o4DA$~3j zrKABbp=j-F4*Ok>N z)lEw8;CcWc@c$h^IWnnuMLV+iarUW?CHg6N<{39MQDx8x9Jk z%>tn2BJeeI=qSj6vF2f~ZE@Ll#TH8pka3}ar`dAiCn=umOcX<|$eMEO(?^Jq8|9_3 zB+$#}wU}Cd0V8I&JN{lQYcGKTd0mhzI%8}P8c?)c%ol9Ub}Yf zLm&Fk^z^hR|D^$32kLtfm`qG5CKl7{c}~C*U?CtA+Cn&N!j*t09V-11K^*tyux{Nt zq@CFCg|>j*?SK1k|LxGBLobv6gFEd2pa|P&Vk`k|`jaoEEdXEx;xUn6(0P#cHyUgE@(>eL=`5I4%`% z=tWz!kf_AbiLD@GNXY|aAX$xDTf@Tef(V)5ae}D@&Ps6VY9CLcnpI9E28BE1+kU>0Zp_d1P-31;2Q`U9Z0A-L5 zDH(D)lx$cQFBGBuyne%Iy)7F<4 znU+`f1g1Jv3AWVMBca*i9OO8R)mb8oxayWrGN|4J#qL1>F~*Ev4b9bX@!PB;+Or|p z7}z=%lnn-68{jB8aDyvtoMjcoYlarI5{`9BMoH>K} z;-EN$wrdS^adh=s-UzJgAxWC(Qp__^o9Kf1Kx&!*OsliqYdT*iq~@ahE)@`6$TLA> zGBKt1-g_^mH*h1;LbNxjI%0uj0z@c@%3;;2Rj^R){3h|MSFe8NGoShV=Re=by}{2> zG2#gG^Dr~tyF3RlGa)gT>HqFD3y~feFF1pqYS4!_;8{LmN<@ z2OXC@Vv{E%!P`VQqme6~*!61UigPnpzjow|{?OgZ&-}_7nMh3|-}_wFC|Xv{o8Cg} z^5oowt5X}t2G_0{ICA{#(TlY^7wLi~hWsOXP=DvOVe4LldT;z*o6&v?vMoj+!Zr#$|!v!W-7UY;bipwk6y8%52LkNeql;wSlZu&I-r|o;w(M zAg_;Hu-Dhh3-{v2ovy%WHTl)ab2Hi0T?9ZgYUJ)}0c1zXByUhFkijoGK6i4bfF04h zl#wZ4dD;fzbpw)6XI81E zdV&#D7J#IgsDmkwB1$Gxw5F^w38RpfSTF3+c9IMHl1|+M$rxdRttJ(RVbFrDN!KBf z&zJDq{!t4ro(2e$M_Zwr2E>+q*wBKL6B0Y?2+f#rNs^vTpr|JY_a&ei)aWw_qh=vO ztrrCo!cvUli6ST!DG&_LQHEu)34)RfE#3;y5jobv3wuqqnE|jCyvsTXM##sB)lZir=q2V3Y9AunnGHq!(7Dk%I#`*V;j{!tjO{)C%S_;{OQt-o07EA|hT z`Ua-f@4C3@Kx=4p?b@|B-+Xh|-Uz`TJ9g|he&aU|9XixCkQKZ@;E3A4fB!%J$NzZ8 z4t}&o;0p8e^N&CN_{EDCOH)_3TzGQT#b=r`)2-R*tUi;?&t;AItXa<%>IHtpH2-Kj zKQMa!z?(1K^af;@z)d&Zv}x0({8BcX&98p-tN-CY{D)_rc_y${kY&4e?fT_k{^i%a z<~8{@U}N%;$v^efQwzetW1ky-f%JTS~;+5 z%eGB?GKsF~*}11roSK`PKYror?Rz#}nQpw{HLoacyS2Pw*UB38qU(z^`Z$N&25Be6 z&5%36#%-{a70bN+)*CF&1KFip93)NPh6x#_Rq>zYygy4A*_drUnC-YT+jCd8=k9FR zUD@_mk+Wud);FxTt>S8^-JWkwCSbr@%-}1_%GGSux@_~!+4eiK-LJ{^-c5r$?#MRX zoUPfB4UO_Y+~y~#jxc--sgkK^C$#O$H~d)G)82S-*XT!QV0dy^;zU*ZvhBBL-}j5z z8-6~!>HD&EyRxA*S#6L(V$vA-b1$6p8!p@v#M0H(Tk^uarAv6FT)&jL(6xbVculr` zXSV0H+0Eadt=+-waq?nTXzF0|K^2ZUzM^!ckvaxhc*LTuvk6BRYPRyCQ88I&uZzt_ zCTNy;QiG!-x-FbQMm#A{2lbxfqz=W$KZoyHPXz(i+|sI%8#Ul!0+Z8;hAN5+GBP6QD?3$decZf(4Bo z!N{|UUy{7)P~-(#Xjn4TCL|oR_)C~Z?OwBECy@kzU=0<6NKO7hJg)-Cp`o!7YO!u- z0*cpNG-jS;Rk1eHe$>sv=Y*PAgLe2M=viWbnb8ljujB>AJFBR~5H&8x&jC7=_6YndnJ6iSlox}0BoJuIqv9FB=Ze8_ z5bpq@^4e19x}a6rmURV}J{eVC(~rEdT=6HPVlTUO>(<`4T{Rkw#~yp^V;}n%_GD;s zWGndAVPaz9mRoMwvSmxx|BK>C#)MzJdKD?+@YKZ`zox%lXd*vsI7KWh#7{=$Im9p0 zm|C}MX3cik92^|nxpQZKe?RD8F3YgD{^&3vQ4jn2N7RE;UXY@ffypTqS(^@||Sg$vpK7HZxl?i8zm2!Fgnz79r z)*jrob?w;L#KhGTmuI)_-c=mpmz}JvQD{eWri-`N!X68yEeRT>#FHUBRE%%Ffsk&E zx*Lf|GRYJX8Yd2+zd5x#oc#hJMYZ4-JWfEMYi##Y%NS}%&M#S zcxMi#l)J(RO!N8q+3}Vi_6O|p-#cab~CW?3e^=$p#tU5@} z3SQ2@@l&d1qZ_iDUYD)jl3hKQO<(374@Vi#8XZ|(hcvrflpXm(M1lO77t>s@+FQk(;2o>BcX&d)Law%zX);Sq<;V9H? zAV>uB>SZ175$tCVNq~t^>2xtey0}m;awpR$AYDYR$j=z$F$+sf z7uSY#aoe_SYuB#L=by;=zwm`GeBu+I@Ec!~_f#iPx`z`e!2$_^D|B&8_tqxzNF4VX zsc9nLH1bTtd?2)QFf(79z^4E5<;$BkZQ8wiHzph$_kzZxp$q3P(#1Zdi$S>yzG}p? z{NnmOXSTiqV&$y)=#k?y^NsoW`tdX47cNiY#Yts>y;C&97Jgx_-dsK0SF4oIUY>!y1Z zh}EN`Gxhn_;M(E!`~r`aH45#7jyI$J(g^Q(ZGcA6^57m7-sq*86Rew?O``^n0&xrI zO%k4{6=FayonFE7{R6XQv;}uAS3I4@5w5)dnVnyI;pt zb97VSFN%dG)gS*Q@-OlD7G5)=F~)eaMV%ISz_pkmeuw-#ca*jhiE@}MD3Ih z1=kRsQ#!BHsniRwQ;?1gfSoS*S&I`z6h{S{b_$MNpn%T;!gRDr#0gAViH4}peE?p- z5szT)Nze?0WL5kyDD`5XG>DlI5Bjj6*Fe}r8X8;CECDJPkEJB+mTLZJWKs?UG9&a# z2#xS7$hgM@PzJD1=?dy%l|x|DDbg-xUm9`vjjq_6%nH#q1;~v8oRTP~Syw|Z=Ax3O zb4C7&O*%}cSni=0{n%9OqA7=pUv!bLUz0i7@`UG5z+!UL$z8AjZ;K(?kt^9=2JL=^ zO)R4T;rKNuHwLX0wpBE}?3+<9ZMk`5>(1M5yA3-qjkS&Y?z`{3?|tv0U-__tZwXef zUj3f;yyu>K?g?#yN6*5-!w)}v?bNfI&OWi~;xmQ0>1=+wP@iqh&$jA%Gb$f8>SGx6 zX4JF$-#otW4V6meHLrP1*HdET{J--%zjOcn_ai$EtQABcAGrJOyMN_Zer5CK&4Gn; zT;CV})vtcFUat>4_PJG$e7Z7qDV-X=0K^xqR*P>yy)w3GeEl8>p`1H^=J2UY(=&5V zpFBS^SD#;KZrr&34R8J7Tkm=E${IaaTz8GW`q@u^`BQ&3acR6Y z%Eil5=^9(&pUaAChWn;w=FeT>6U&3UwjA8G)z@AuE(D9Uf!Wb58*jd2WsQ2#nJ)gF z(ZB|HPvQ+TG;xPyqNqD2m@GO`uoS4hNA$9|*EEjlr2M?vQYCoTmzWOJ# zt+()L63|+xU%fnc`qcc%Q-zbKOXK6EiOH!G^>Q*}zQaEAI6uqk$_mjdY4&fOWH|D2S>LJYFw z%Px8;rfxDN&;`o_lxHr~Leyr`6_^V*$9TITM7qW>&Il2%d zL0fE67fn7*a|d8o5D8sjOa?U#3z?IcGR#*SPF*Bm#PY1j;v4;~E-T^&@BvDoEW%#* zTMRWsG;v&wWRqs%bm5m1LbiX^Phq#(1VC$oju)Cm7f94x%p_+cQse+7%6_`^0kk9( zQlJ&M;4$<7lFuzy1Qhm-q>I;f+`4h!L2L%Bm(XZ5{@@S(-~%7{0Eoa^!M7UNdcEsi z@A`#b_=T=7b^{I{K79Vj6YEYrIx_xD#&i+;oEfK!g$Dn3k#C#IrHhyMzj17A?2bF` z$eY4}__2?D4C}a(^DhNt?3n)_`>`MMnQq{ifAGNvFJHdg_vmMsE>2%crzX!@UQyaq zz0!AOZ1ctSdwegQnY{9i!>7(%xO{f}+AP0&rd2B!wr|_`>Nme_|6OlhS)=EH<=5zI zpSkzTpZetGix+qwC=@D88p}fieXB_OvpsvVRjaZ}h2Cec zWry#}KKCE9t7q99-I#sVM~@I1LIMxL$U*HPW3YZ661ch7axL3=NA{L~z&D`c zevBEMpP9RGap5ap?fcS~i-!+0Hbh9X0s=O`N%VDL^+n~MlbvO-jEi3MI$d5p%KzYE z-~0T%gme-O4Grzywd?!ux%=+dzItr!x?;6oldzDTJ)Hg7Z)L~6p4I0#CzzFZXvAbA zP9{%1^?LR=e_=12t1T%&fQ52_uuM8^wSA6aVp`iWrn|e8tfJOtIn)q9obL6d00iar zA+&xD4}lP~_!N+2$`!+(mgLUr5J+&-#Si~2*$XPE7uNhYATh{yY;49zXix+*aHYO& zL>IwVw24AQMaCSh863WPEsQSE<~V-92Hk6Aa`+kmG0Ozw?z78?ARoQ!krj_NYST^V z7wp!c=RmR%Jvj4Xobgp!3^^#P8M~=SkYzpx#B3-CpNPughKDgxtyB)Njk+6bX( zqowQRiq^8Cje+E=EJX7GlDx5xx5-0RZX{it*?Z^i+wNSyzWYPuuU@_SPyWe2dGg68 zLwf};4h|eR@Gt-6zwCWK>iP5MpMB)(qt8A#cIjDu&dvNZwi)?!vC!mSwQ=Y;UA%hm zP5bxn-@0{c{*lPU#Kgb*cmM9b`|exG`IiE2-1ppb&#(XbuMcC#7_p%5);o0QQ1#(Y zuX^;;m8r|=+~d`Rr(`(okt;s4>($L_zbCyuH~qk4M~@soe`R8-zOYa&7Y3@uHAB@+ z8`mFv!w>Gfb7hU550+n}2R{Ah-}uWvzdU}i(QGxiF8t?#Qb-zW{07EK>C)Bdcp2vf z9GIM)gN0hTeAA9id$w;vB3T|78rX4gc-PGUcUtfl(P$MN9-OP^+dG4Cn7CJLSQ)kaIdqgj8a=UkDxCCR#hu3C~pqe;Mn6)=d)6U zDp15Evs(=uz~WF+!sKA`qYED1G6kTrO|>LILjiV#(Ij{FXbTC{l2d=B3ZE{*UCtu_ zMOx!IqAnj5a_1Nvr(s9-4t>bv-Q|HzJ4KWzrl?SZ8PwC*(HmVL40OSTK?@cTfnU^C zNj3p+Lgay=IAcwf*p=t&Hx424L9+yDGEJJ_hIG4;lLvge>(c8%=nb12G^02@&>(}2 zCHeIOHM7`^k|1G$!_C>!Xo6WxRn7VLj2-%<<#by*kTl`KWBa~Dms2v+> zFNK4%Bzp6?Vn8rVUB1|qSP2drkhbjjC|F?H)-L?Sc3tPBB}qX|f`j`;;HSjk3Uxy5W(hV+{vKv(h)YW>cq+O=)lwlHf#wOT!O>QwRU(|zZk;r}jnAe^%GN`L92A^vP$=Ow7zQ7h1JSVfA2n?QnH9%Gj#W&AZ07tgO-V!SZW#?)cNs9z8rY zHB~K@%A8nSr57;&^(MbxadLWYe!jVCu&=MLQlAgW9t>9XsBw0_ap~IBwaMu%>sRB# zSQ%a4zh=YA8ug+xUA$HA-sEmocYgle3a5lQx`Xiq@t2I{?vPp=-H8$VwGR zG^eL#Kk&zu-}}81MlmxkkTUW1MefZ%=3XR#_d-jye6=saJ!EE*R?CGy%b5op8oh~|{{L!HCtKnU{ z&%GhZQH_ElmdCA*UKgH{S|A?4IFij2KgrOZ4d!SGJqlDlk*-2i+;cFy-ynub7P6&L}cu75Z3wcBV_A7)A1MLXf=n_c4;c5%CWP-~2 zMTLpN;c9!K1GSo+HdVKW)Se8z(5xQNi6z2?{kU1{*Dw7q(cx>()5ckSk_`FCC!U;XM=zxc&3cKxGHT+jfn z>kB}#aO0-rwym`kECA%`UUW%zHG7Ff8(o~P-h?hO!yMF&Mq}T;eK*~7lcd_f;WT;a zLh<~w12b1z$Q2vybWt~19*aEWV4>VMwQhH5$IaMJ+h)S2KJ}@GAAT4voer(baVlbD<0bahE|E6-ZnQkfAakJ0}ns>?3wY|dZSQm4b+P3hO1+P z)v@8qV1K3BKQOv^_r~1^SJuc#wD}2%D&}+J_~tc*S;;UzlFzhoJ6!HNRs3xgwIl-~C-a z`n&!1>{ov^`x}2Fd&3*D0|&B=8~Gs7W>IEmv+?olFaI+8lRx3RRk3Rs7|mYsL)k0e zMgrEsL>B}ghTP1-xHjR^XwWJ0NjQaT&+JdSANs_ff9M1MV{Y;)j~pI>w`Q;XsjSi;&K)LZ@v}z3 zAv4xIr$nO=dV!Jf1TRA3e2sFH+_trrf(3v)-HR^Cu4XThXrqhM)tk^IW|)H-lZ>f0GY>3EwOYk; zh2x_WX^nlb6$-_%v9T^M6B82;Jn+EG3{sr{#u5FR1DI*#o2Dhwjntb25}k-d1E$S1 z62A+*{6(9R>V^e?M4O+GIPOir6k(z;U7;;Nj8lqpCY;d$p7*Pxn~^J)uuM>&pMUtN zXa4g3hfiL-Jm0MM*Z5Fz<4A4Ys@l4dT7RulD#8l(o-1qA7E84G35nz0gk{!fpjsXo zEUzA}tX)+Z8Z6Cc3w#@JsZcAGDzMXP)f-nH`%AMffOqlHn|s@d*4vV(tx zPl~{snV$R8KO6e+hxrE7y8iEc(b5~(cVM}0kS}Bu-|NsR7Ld;y&z(R2(NBKrGoQY< zF*i-QzM<^4w`F^7=TXOmsuGLR_Nny(wo2ewQncIFsmzk%Gr2VPI0N7_yFKQD8 z2p0%I9Yz4=iqd75IS$E`DkO&?QIsz@1t3gExj~8D1`WE{j!rT237>Hulx6@m`oUpp zrvf^7@uZ;+$^?Z~5}g2=1{EqUaZGMQPw)snh`@=$iDTqk3G(h>LV7F-D79mj9k6Ig zM1YyGX(KMQ+35>n6xY~Y>>9sut<LLPDstLRf6CvqQY@vjHy-8K3 z#FHylYN|yByXYdh5h!MY^IyAJ^n$(qZeNQ>VE$5vE= zr{bnL9rEav1@di*G=mL_$w+=_u#c_PYQ5+VdG%0b z%}{mSP<7*~8XqKPEhUXv!b1il0095=Nkl=2Ht7WQJe;D&N1F2by4_t4ny9 zf+Xj`2N?juKrpL!@T>d*gjzqD2h&6+*(qVH9&*>4X(+L0yfS(DxS-96oauv(8vZ1|NgMnHYwdyX;sXnc@YlQa~zg9LspFH>%BBX# z<_E`Sre}|wIP<{6hYlS(J2O9DtrSKEN*hP|)~~9r8L18S^PwX2cm?4<-_hyH8UYeq z&fro4ORW*o#lC8BsIN3SSV7V_HpF`ENM&fCINMr4wqE1AQwz;zb9#3E$nlH)HJwxR zlRM)muz;Q0${GO@?BHmNlK=_bYT9=&1BW?oeaNHI4UjXelO~iLtXryNn_t1FdBE}G z*;l^8Ps(}gTls;dxHHYpW)D1&ec%JxAO2zX@sDSR4|gOOsUV+(n7$B`0P*E7li4M9 z-2z3}K^VaBx@_|+`LB$=yLyZm0dt}F7F>?eOwo|x<44_-*Y3@By^b6GR;zLOO7*vY zr+DhrbyBZoQ->vP7X?!sjdKenOz*HcX{j*?y|ppvF^(ZJhcR* zi{*0p!ua@q{JlSzyr#FL;^5i$27c@f&%Vwh4M~d$h{+5KcmFt6bym7JNkXcm^a&-; z1cCu+!5aBdmv5JgS}gH=cNnPML}8XCzE63S@K6y40JkT!kJy|mvc#wK=(2a%2hBQO zTg-9c(A+C|IBm`Ct-=39gL_=h{3yptDq3VXY$6!XrQbF<2EE!%LBM2`~Tz2p$z9(>7R>ILHven z&|GGZ)_ z>f(JLPE$zo5RGXM3KaSFQEAam(4>3yeNW;J%%COBjTV-?1rI9ZEeuV^C;)_}OZ!#)5p$dj)a7>R8%x6l-6EG*G4 z3RNi=wA2o;B!5lxK(b65kSM~wq@ZLgG-eyYOt6e54a%h$;3#ck=42b3No9#ZNb@LW zNp=V8)ZmdG5=Au)i!fUY3&NsoFdzg;F-ZbS9?3HwFGhJF>_tgrR-aaLbeWl@kmXZGQ zNI#p?^@UQgSS=NClZ278mnLTB=HoT8eNDv0vsztQqs0+-JcjN+@!q97d`c9-^!6@n z@`6SO5))HDww<{maP1nuBm=nf&TQ>k5)%{Ihd<0Wh$8VrB7ueX)2jS4D>G51r}-rr zSFf`8n-%k7mQ3X!~tENBkJbw;~*P zI0&T^OFXBX7llJqM=7#IY$2R|Q7osD05KlqT7skwnYLZQeuL{|Fm**;m@~g4KlC*r zHmM~^8%!UU3Ond?gv`xcq9YU!iehMr6@*cuBQz-ngBq9RZQsq{TrwyIwUlR=Lxb|j zt~7yS(pt1^P*`1UG5!4r@qnNgS%!)i$YX&RL&CH>4Aqdw5b_sKOS%LxnPPFk5(|)) z6#@TUi%_jxk^kVtSqf%pQJ%#RORi{xQ5ZHyu*Fm(u%OC^e73Gw;1dKDwG(2C3@cGj zE=Uu0rSLcnw84M`zlQMen~Or@mUL7x(V-J~5l~OI8&F#KOYD#Kc7J|M0BfMFOT86OL&Q?TAvT+`BpnJhnxYO2ytsB9oJo{9w-& z+%Pa*n6%J{;KM82vLhz5(#Pk=zWmUkC!aktJwI0|^HXg$^9`uX75n(Fran8dG=86i zEYn^A9JrYJaVawcx|XT6wG=Nq=Fw-mD;FFNbH0 z7p_c=4A)u(-hgzMAh22M?^{`;#i83x0`Gpj^N_|7j{7{Izg~vz+A?V0f+RV-mfxBP zAi+a^I5w80PrrTgNq#Tll`D&+DCQ(4%H_*^AL*w*%>n{ic`-{S`7VA}K^O=IC{?q8 zHA)nbReBd>AaV_#iDD(|t8fQ_Uhm;>P;I}HPffEztJ!GX`pD8KbvEP2i?w{)ls3}ip_L)kz1 zC)vOGH}84Rdwxv-@vFc3tM7jIyI&83FAw0pflVB6`pntSeep{;czBErt;zP@#cvbE zj*>H`V+?$k1RQ=&MZD?uv5Gqv&(lA5iJtMI*ua69N=K=!XZUKCDmrbn7!Bqh24QWu=6yPaAHOuZC}v~ z)WNhckZh)rQc{JcR3{@ThBRas2E*NS@a_VwB+o7_D4j|eOk7EDRiQfC(KQi=&|ysm zUGiu%nF?uGZ1C@;bP>lQs8^%Z;_&T==+shj1QH`Dgd79iGOGuPbmWR0nv;3P9^T_h z3o>DAhEyWq(YQ%7j*66i)e+?Z5^4mFMI>nsYFSaOW~u?1^~yNcjdK}<`GQ{#RihAz zHR14;C<02lK&uALoY3<_8w6Fvq%slA0yL}Q13_YeLs&@Y;CDiNxWr4KLx;d%RKTSK zOMu53NVk?jTbX}oDE1^ISfH7i87|WbUJ@)d;aZ#}(#5#WC^&ZeYJGtpaplUDfq?-Y zPAj-EV7f4AVfrHa`uea4K3yJc4*mDpwGTdf;pBx&%~rF&THG|!w_&8VZnQQsT;U&^ zcq;_3l3dHMo;uUeRa;r37mP*M2svH}c?zbuRIFCZrCPOEDc35c{%UEs4;f%-Reu?4 zsEn~#(=*0Kv-#N5=lc5Mmn&+icpmkwtkL4|ksX0sl#gowGw3gtz-}pBla>PY(oIsO zZ!q&U4rxQZPI`Da7>{={An~mv7D9r`P2mi zLfHA>alqBeOcX2nPn7mk#}-v~zmDGnnY9`d6O~VVqA)YlVLuga7CUM0AYJ^lGtWr^ zd98f2i(|U0bagE%W|@3hG?Ki4IVi~e?zt!X{_k&>H@=Z2HFxjsF!Fuhm;G=5TlUZX zS@w&+n7!c*Teof<8yg!P9o?{D!@+|G-}bh*{q4W~w}0kmex~;$7QXio&a`5&*=&CH zbDy7?n&jbxgK+QbvPxh4o5cc;Lmq_Ch$kAveE9hgWepm=5+DaGwOqV6hkSyMY983} znOLx+UaREAnDeM<*X{)@pMT^f>}4)E)6wN*`2*u zB-w|qb3E`Vqb_jB3?c-3RhW&(sQ@4hF~*=~WCy^YXC?&z0jSu!uNbJw(U37k4#w~+ z{mKGex}h~f z99Gc+1*2ODTLsygKQEN_)S-&HlC!+fj?b*Xw+|_DR%F&p3#eD_M04^mk$T6hg?UKE zqcSkVH~}sVNL7r$d^DLi@@(b2`ix0gA88&$%GM}u&mlHav=bzui)CliLNQ24QdeCG z)`C&yrG^-k%fVrYpG*eOu#9usJqe*_37|W~-10L?aod1_xw*OChg~apk$|bjg!itS zkBQJ&3Oq0hm1_4piYF&0*R9k4-K^lofa&Uea#e|6*Z%d!(5EjI9zAmE^5hgIdvvh8 zWwf?ol(}MmjqmV?D}`m`LUJv^h-WfZvbwTHFBnU$k=`VsYZ?ESyI3q&%KV(DB9;aj z<6x~k(qC@07N+N$=lZ{GYZ zbJqx3saCmqEt}JS$?Fc5O-*ITj`5))$~V5jbTQn_drHbr@?}xxb1msYJU_<(Fgh0x z8Jhxs=^{4P*zNfX<&p|s2gkAv`zX24nt$}M;^D&~|IE8U)0|#@FVS1vm3n(6yP92P z`Cb$o>?&OXc{<_S2&PxIQ5P4iyS53GaugND~_b%uBHO_+sJB;QR~DF(^)AjzUaADvnU0kcHcBa6*Mk_)yr^GS*r! zl2blc3GUDBQh_BoLe_X(LR`N?eh0EmQDA0j;P-4+HXJ7)@GNBcvR@dv6EiC7!nonX ztUD4+0kefbct6!c;Zf3DI9fj9!aZo%doLKSbqW|(%F*pNt{wVShYrg`X;+7SX|0#` zfccZQ2C1|nRHb30Tg!S_9_C~3=2>9~j(Vux`VR)IKOt)LPEJlr_y1)3pN;zgl`0)O zYVs{!{=A;Ovz9SVvmd`#zW1569#pmL8Gj+PEuF6 zdJV9R8&cz<^dY!ODQt+zyD~d+^dMf`KqTo(q&FE9{d;bf-nC%X56wzJB*6EN+4W4*E?x4tZ+^=iQilqd|}$7hX2LXuBk zxt0<#MJY(1ThqKixZY6w`fVmjO8Vn;R&l~jf;`~veO9RWTgb>{fy%9fp~XT z!bV&o%P8Un1|4pJn2<-Gn|jl}O6^EoZQW?lE?14XF#cA=@<5bwFU4YSsZ>W;F!==ET6!+9|oQR^e4xaBKF#pMk#~*N7LogV8+Srh+ z4sD@CE}+LUf{(8V{VmaEED(JBM;q^ZrieI6)GN_1jmE4iSN55xU=-naHmJ%7*i?S9 zz`dr_EZe20o`n$ZheVU-ngsd@+oD;{5BJ&Qv^wgO3fC(Siq;?1NQ1(!sjrk;vZq`u z;3B^6OT2~6BIxGwPe}zGCTEn1?1!mO!>xa+7B?{1DV59GeqV06-fVjy@8Io4-t42+ zed)%%BJaINtr>zB8;NPv7R4XXR3up1Z&s>Z!ITs3IVxALz*H;J0G6whBs*n=uX50U(v5d@ofsF(uP;92x)gu1z@?` zjOW<(ct-7c@tAYK#m%iC>x?XNJO-jWz z?JWQ}g62gskWDO>oiS5-^0XtdYl-f)*Fa_v?h!euJ8}F72?Q3lUlt`1S5KtYYs_2W zfRe%(k%y_Ndf$d20Wvvb-much_*jFZWBnCZh&lwZY2_Di1<_+rgZ&L_GvMnh0?h&Z z@HEfat>iB(v8pz2dze-6($P}vHz)r0Cu8PzgT;GdZ@|v8iiKtP=gDvC`{jqf<7FEb zlVK9JOg`rwuv6mY_sJu-nYHo`8sELrM~hq|ruBSSQ0CA?3LCbN3d;Aa=rUj-x0^nf z0TsXJEUg1d*oY=W-cM#txYR5ad$=JcLX=oxIv^!OBgEx}jyPz`M>VXEVHX2Dk)ICH zJM1}#H|A!`*)RJnej}HC#^pFrOUe{fg?kBw<7m3ie}v2=1Q`k3j_TUZZYN< znRkK2mO>_0m`^d_!-&l!va7+`4uX&fZKA@&0$OJ_eJxTzahlFQlftpAGyWwWPLxD} z8%|SYHeSq)XI9{^i_CU!JZ1J6qJQBvtL(Zb6#sHxEscXFgXMoTpP3JzJIX{M#^hpM#$W$2o(&Z*WC&siE(fRMS{W-dk8PY^EFkpMOk zyia&{-eTQ554Hep$@kYs+UHD{J^n68>@I@*9uDwpuSZ1PVsO|~OkOnVHR5<@>rT)`jcAMo+NnXA55XV*zCHHxV(n?O|-c8>o?#IQth9zkum;6T0D#W(^ zOf@xKg?X*umCv!$5}}Y(jS&31`!oRUfM0{IT^#5~8;5e!y&8o@yh^KNQ-hvZ5AU27 z@i|5OgEquz*|O<;3fwgM&u}_=Y;I?Gv=Sw<^b1RMRl;0F=p|IdzHJUzzRvGWnWN)> zKGpZGGF@gqti-0h0meoYHj_#o9$dKi+A@@b#=B& zG~&ua=b9Fi1SlZSq@p(}j;i_MfN=mNaN>nKjh-!gjUF|D;BQBdQ1L6VG7_kC2daiZjc^ zN}u3X62Ob#7aC&mOId z=V0wG6{$8H1Q`XD&6Ai$Pb#)6kK78sqoMj;>chOj>70k6f%O6~Mm-3zk8C-&VNIYg+bb zVYJNSHAz}hR`h8u@|(!xZvxROp?~N}a}aL90Q^CrQp|Mok+>PQC4$+Giy^Nl13M%M z0uSg2rYbW$$5G}D8v?LPD}2?cjx7@YKZmf08#vpw`Xf6)Qc#Zjb_75CsXM?LUHa=K z3J5CWOVm5)x}^z%PE_0GGF-vvLQF%JNR#1Qn;2Qox5 zH^O7s1tjOO8Mb?&2GLgEe~r{mvg7)s^sny$rt0g8ei;wz+)Z70RcWOI7TQD|02ILt zdQ?q zZ+uNH(xL~NN%uVCT8tL<)~5HKT;)?;IG0u^avu&^IYN@mM26&wsI@gzt)gi*lMD4t zFb~AZ6MIjRIlb&ZtOh7OY5&Eye>~f6a0rmtEH|)@G(^CzgXCKl6QseuQY$SY1pcZ- zYnYSE4pXo6{pJ&_MdwhFioL9C@kgAvo{Gwtc1CHySCF3+_p-v2Iezop8q`WY<4LK} zPflid$1i(MBuiRW{**8NL2Pq5n`igy7<=LIz@PVIW9RpILa8^ya|^KrgG{Nh<63o? zcjOhCa~Li-;-dtJe+95M_U&h69K4*Wso6u?m(xCm#TVfs~lN`KIe1Q8864;pN8m;M3+1;CrAvykJnHl zA-zNu;?vca0FK5e_1C#0$p^JDb5vryu7{b}cw6B&&5>+IP>B}iFV99KEFq@ykfBqT zO=_J3#U z42hyxyZRcS-H-TQj_5yRu5?QNC_^#J)xLCMZ&KOM;h?zZJD(j^6NrRG z`eGP}wv9XpPUVC8RB5HrWHwKxIP1NFf?u4E#NEy`Ib1Wkc-_t2H|OMu?}yGryDKX* zxGD}aq)MX}WQc%*1JcWC&C_Bn=zDM#JH+82I4y2Y99}&zFE)4&>wXw2A1|<}K$@-kwYJnSKjnoP?YsT@T-#hc>uK(mb({uZkIpl(l`IT4 zxwJbyYIqR?l_Lv+H?Yq~#Q9hKaw$ZN1_)#9y~Gn9mnM4vpGq6)S-uC2@ADGvWhA zfcI}Re&2Hvgkt@nT?T*}8L;Z>dr#@-^z;1%h%^*F)d9l_*80Co8sLxvoBfdb+HrGO zYkDj|xU{FtoH_<@nPzf$AwhKXH`jLVV%;|#NAPzNa~Ps~(ct>&q6fh7`t*wC?$ECo zff*ELNX0}mnK?A^Pig60r-K(@G_art!RL1d9L_W?mvCX>H+A*3J7t%lSSNLqlhQ6e z{eTX5v}rIB=u`h5X%nh^sRgCX_1$L%j4S$8bpP8GbWeo^Crd(2Vr_+JNxLrTcb~v< z6%X1%aidf_4EX+z)(?+hR(%q!Q{JC~*CN>w3@yaRMf(`n@*}s=o2b2-^S2W#OW5Rj*%Vs~Py;>^a zmf+Hd7i_VibWyPyQOncl-Fo~oVSW-z7`(st#6eB}y~Kt3Gt@Ksec(6wee=Wn@$w_^ zA^074S?1T?WS0d#AW2H4TW$Oe*jc@v+Gw%&TL1)YsR%x)0KAU0hG!E7-}gQ*TNag! zs1B&3FAwq#<_;WckZmc$iNM>}pj;F=b+6rE1aU%3{uJCUz>0kdm!imDXbD(A$WX_o zX~Bq2dG{`rY;k_%z^no_rncKLI#k^(zUN>q`93rOK$L)p3nm_P1i^k|IH02NMj0M{ zVn#|hQaJU}G1Xeoe03PjEv9huZXx75p^$>%eNp6Ki*1nmULNoZrd8YwGCZJ8T0V(u zp#FFx7typ~l4gU6R@ZN)f}zcM+_{a}t#iBAzo_-GE1g{xWg-BYQPo@E~{|`lCyZxSp)&Kv+ z;%E1>g9u>>U-JU`5jf(~F9#F->14K{U#NjplF&Lkt`M0PQ z>DA}X#@F9~ibL@|;rJ|z436OGY#&WYxKQ)hSYWuF6gMNh;>ztvP{i^ka(^N6q35Ih zR;_;X4CgCLd=8E{rP*a%YLeAjyksd&JrmI?tz>6g#KC*KeOqh3@pXUJ__2PDbvG4! zU|rk!`S`5=_Is+WC;VWRG={M&)BE}A*?FJ&d0X-OTJgJD@x9XUd(!xC`SYlz$>F-5lL6yz*j6hTicjkK3F`#UeoqivyP$UST`iQ%U!ZKAqxhfNkNHjT)L)iHVg3r9T#QG7cGB)@H3J3#cl=1&-On z%SZ_A_K(OmbCDw6Mfjhk7EawdZ-({wdn%gcc^7W&k<@GvRSI6@jB>Taw|J)`VfhcS z_}IqN=mE7%G$+dDbq=z=Z9Lu;?B1s&?glUL=xqvE(ulS~n$P~Me(gaaO>H8?^`2m|)DJ+wuq0;$+Dsx?sef2q80#@*GVl3Krmub71! z5lNV!!Al&sxbEL~5_;q1^8~&=dVW53K94!SN@tZT8ht*V^FG@0KGc0uA^vwCR|!f( z{;y}ubbikPBb3yC{pYd6d=b_r;`a4lC{x`CK~_eF@V?J(Zvf$Y0ioBFjs8QY227-4 z7;<``@PGZIf06yfeDBGrp_M7pn)tOB4sd7`F^-x?d3ct;^N{EEXg9xyXBF~KWZSHz zaq|<~$ck+a^EB1~>u_YZ9%yBRO9HrtHNVLrc&jR%>O{$zmCyOOOw>&z`+0N`N>;k* z_TA%%@9Gdd{BYfu2!Uo_Q;LCoMa%RlLs%fKmYRWfxIPt*hJqPpoasWEC1l&a!VlXJ z-HzHWNs7L2A*vuc?4#RJhQoV}`<=|8uxzZw34&Rgd~<^+CLEJqAn|Zr6cK8~5y+19 zMmJIXJ%L+Be~dBWLc2`L#kwpxw!2+}dFF!7ed2Gov9cP~b&y}bvZX)=0cOIXsI@jp$9;MA zqTu2mZGl+2n{2n^DpJDQ_(PuzWoMHhibY@&+&%FM44X&#sMHWT4XHbPGm}h5k<#t6 zSrXwA4W^2E*!2vyMhFYGnkXZD{QF+}u;Bfp@R7{nYoS4D?5pmFK(1@BOdo~d^@fXi zF;dKk#eZ#TqYeUuy^BNb{#q$&{8`M$Z-$ZL+I0w|6W$XV4wW{%7yCtSIzz3p%@pOk zr#`UY)YJjBHc5QH0roDZlRQ6J$}67M&ZGh(LMo#2K-2#L!tn+p5>e<4nh&b!zeF+% z)W+bJh$40ts*BvNuYktrz27jH0$c~SIX8!I?%(b;;b2i4pG|h_ucW&?EoVhZha#PJ zaHG0-@dVJBI)4CnHel@)FdcFHlQMAxSSoz|Vg6 z75{MuaZttbkUQc9;}RzD`@D6v%3w2JIj-XjrYUCGWI$SFEtlw;)YK3ReKo$zqigPAo2VXzNBu{Dl0F@#dz zr(B{;mjdxlGm0evIX>`CDQ+6weR<#{biNUA^9iW7`_&!Hj0?-{sA8Sb@Q%Mp`Jqk> zHwCFp4#6&9ua*9ZUGNab`qA^2)tVV}@pyk-ydeh1D_{t9NU78nZx1T4op-ipcxEsN zK#lHrTDZMz?S1Ue2qfUYN%P7)e&aGbkFL8I`<=nlMp2M;Ej1%I6m<(+gLd`lw+rpI zvs7q$D7}@WchPiZ|Iw1>nz^L03$Vzw`fl)RzTJM|04UOS-5*;q2Ja=qJ6|1BKWBc{ zfMKP@g>&9R725Caj2r2#ZvI-_@PROeTt3g^$&82yXvuwnFPWZ48NZvGudckGx4fU} zccAt(n5#HD%Xd^SW|eiD4klKqU<4(+N}}F7YS7{=L^Y)yNCmuZJDrRIOe}Uw2cq6i8E(X3GaC*l|6Xn}Uzg0zUTG9(%vyQ@<^x0e zc#=Bmjt6eoj6uf+J5_VNOIUD>Y~?~#;{XP9;<3n(zQY5hfNs06)lDQk!DBGV`vR^Y zBkI&hDxz<}=Wo!-nhPFOQH7%j-5_&eMQD_e!hca>9HP`M*TYf%*O=@n0G~*M%7-3aT>s z4K|Zgj`NqLqHZU#J6JBBEEBTc+5tJZUIJ<}t#0&3N&N|OS}ob`L@*zwtqpbQNSWrEXu)O8vo zci;9rQPa?4HyMs*eOwUrhlVpkpmYEabAUZ52SG33QN>X$Ls^xF@4%H^UZ4w~5&XBR z-{brB(*43Oh;)5+)<^R6pG?^%03XUCYY64cbw_H@8saSQ&8={HPWAXAd&{AkE73hX z%)FbRe96@cicOs}6BbF|`fmiPOMl2L)?R#xS9w53nHhm@zx6b6Y|FTAc6%*=SJcmb zqjFj>IaaHrV0XgdYWM&x9?_0^oa}d-gvr#yBXe& zlQ0ziaQ7g|r!DLMo)A|sXFWE1P&d=_`Mlik^jzzDak?;bAma``@poR5u>CpSxebci z+FGj^SX=PRv-`&Odzzz_2b)YWyB3IBoRC8+$mpI5l$eX@Np-z@@HT;b#_WEpZg%#B zM-eV-9eMC%x#t@;{G$YmjS3Th(ME05$;4AJ2|#h#tegeKo$7+EuWXR(o=a?2vr9k+{}25}3L^w6Z}5tG+9; ztXyMI-fyW{U?1$VSa?nm7li*CWu)2UWi{0my`F(zyIAq~(#@r|3ajOFRugTojb?Lr zQMqruQJ&t6f($Ih8A4kDgH+omkw|w zR4V$22qjd{=MLaxqG77detCmtgrOxB_HN&L zC}VWRO57GSLT75ZB)$k@1I`fX0jS)ec3johFMKR|Vw(Z=j#>3nOIkNmeEh4S>6#GC zr>geOSyaK_M9C9ows{=CH^aA7k#Z360AR%408qg5l+9`(AAktKV?LeT=5#a;2#f{X znD@Hw?wZ_sf9{5U4)Z>f&}r3OTwRThf_36{@}DjnX6k8Y2mwBpn$COa9$?Dw;P+Dp zvl-TNzrohvIct7fZ?Ey)n zW28)?2&YRH$Qb@|iw+hU z+)OrG@U?B1e{C+8(+Nn6JkNr3FxruIgs&l-?TbL-w3(jtg2@>VHLdQjL4FJ35hey~ zQ&Jn?jJbWXY`igv`S`5a80h>wmR)>(2_Kb|VzVmZ)0izG+5VV4U;bJsyVkkir!!eE@QN-%PO6}H@SJL8}%L`s)R2EP$GSv z#}da=$?l`siwUI0OAj~K@+;6oBT6z)miN}Gw~(=NK?IJ9P%%?V?1?T|!4qy%igXQs z0}x_T@s3@F@itVI$z%jht%aOjibExA#USYp$I_OlTrQIVXm)`?Wn?uKM&S+H0(-=Z z=mUlod>uMMP@vvo2G<}?-)CGqc_mM1AqqMfAH2o#`?hPc*kzVED0?F~gDGsY) zu+jA5P}2XL-_t0JM}x-aW*r(Mpbu>oM^DfUSE1?%Zk)*j6}*k39*BXN%cgq=({$Vr z8R7q+uPmLj>;dKX6T>`(`K?E&e4Vmhjl2A~V?~7bGMUq`S`DISuUkwe-VI6Lz>7D= zbK{ZIAaDlkYK%ajzeUAf;SW8>itwfz2WS<8YwUoX=#&7Rh`?^}WdiOgP( zG5tcProKz%g$2`?2Tn)>ODD>w24!=3x!%hA`8=uH$HvDGDb}Q8FzCk95ul4`{ouC| z!lqyfZrhoL`QJMgIKbBwU?i;q0{LL1nUd?idmTzn{rgT+r^vpJcqH)YzijoRG=poz z@LUK*f1G3hP&IvEY@!n}11_hPt|1DS5{3)rmJu6uTgQdC!ps-+d@)-Q^BfDibN}wD z{2j?8giz{GBmB;(LipaWX^A%~1VO((3LpOy=|G6FhpI`_mhr>;-_bdr@p!D#!xD#& ztDL{rN(<&MlDnxv7{BrKv75i6{2Za^Fu0^h5Uy7r})=#mJO?U zer#Im`};ZP*5dP+g1Pfa{dv$vgB_wNq#F}Q_X`{?tDYk&9=EahL!Bt7iO>-3L?7rM z=Gy9NlfJwj~3IXJa(4~u)r z;HJq@RBLbetSe9VxKQ`Ceqz~u1kIXcPAw^p#IVNO?bVv7WJjDp3fqPkPy+oI{0Nc@d4~ISDkfKdqfdkg6DvO|%*> zyjpl~dW#B-+PU`RtJ5Iu-_AB}Gdr{e{keY%SP-H=M4;y}^LU9&gL%WrDk$7Zdm?5} z(+b~r;m@pUaKWk-rEAhea%A4Gz$4pE^dhZP(5jIo@0hp^C{XRI?aE}~z`T_pgegNY zg&}p4di(j0hEpl6JW}wBK;^hqyFWmh$^v*tH)~Ntod?P8bG4s z_k3D+FnXNeNI3aiz1hTd>p^V8PmO1}@PD5?cXls2UO#>a-c># zfIxf$Fg(n=^ZA~uz)Qc}wjG6Ikrkkx7~i|E?TnkWm%WwWW`Kh*Ae5m~?}kV?qOOLF72HRvus;hI zwYr9^hg*;?XuNZo*+F8Xvo)OnMpxg@=vp>Ry)OKAN5mSRWZe6S}o>akeMQyf(DYGgZ~vYBu%??70crfi5`_+_>Yo$078I zqRaLLsq=T4^kSYkwF^Yq`i^r-jA_JUxY|A7t!i=6(djobJ8fPC18D{1GN~?4PtFj= zJXd}k*}4(=2Pvd0(T+4C*hx%wg`cT^ng;EFWM`aNHS&!hk#zScA!_!DYI$c(Iu$&*d9#YWy^DXJZ@>4;z z#~6ia&nFF7k_-WLkQ@#7zgJ2ARo^90L)1_qyPc*fOnHOg_Qwl4&R6<#2D2^eSPK=Z zOx_oXchSVAwr5DEAPeFnIsQ?Rd8tmd0=7hfEuG-3_nU=Vy#$G>1jcOAn)zsOh|1VP za!TvByX0i~{h?Y!Co?jtGM=L%n$tAfC3Fg+H*={M&k9xHU;>$jVq>o)b2?si!a~2N zxqBT+Uz25R_!O~w^}m4do4ZAQB{H3oIkRm?pigBPYpb1bP04U+{^K0t-*U2b&vW;X ze55U?C1t`+P%L;!GwZCGdG5&*M}Qb&pO1~5XU-kNeK%LvYVD@;Ai_@~j)1PNF0ABy z9t5gDki~pBRIL8Z9Xrr42VVA`4T?CX@!THA*#*mY5Dr)WwKm_c_nxnfo~?Q_*msPH zdl`nd!-l-C%Sd!u3f(i2a1tsZ_;IEH;>npCi@p2XL+KTn!+tBW=@Z)N5vL{&Hg)c) zFmwf083S`&50gP=s9yMt+H%KAEP=t(EQMLJ_4C$MH?+5hFD>tamav||@GBCI7;?{a znDjuC;Ey3u!<~i?=i)5%VBO|(WQojoVJ%oN<;6iyF!b^qo_PL`xV4(FQ?Ql=&Yrfb zX~4IrzPP?V8@_;=svvphjn`%g61Z_Ru_169(A?6UlBXwHG9!>~*Oq5wSQL(7g1-zk z?*RJfh1>x)AzCdr;0(up*qd54zCyIY%X7}Ttw@idbCHux5&iQyABCmd>}0l4D0V4k ziimGb&E5oP1&FRtM)cVUbqA%QaEI5MY+%041!c=lI4=%G2+uZw`w9FZk`MB-ej#Zj z^WDUIiM)daqaQbn@m2M6Puv5UCRmJ<3L_c=G3j#C%*?AI86EKn6Dr+V@1lwhI+tPp zZAL~^mWK+7q|1Vfl=Sr{iiN3325D5vVry2Fr&5JcgeOR*fc9q0{DnS#O}j#XP7TS; z8KV}lfyKJaJ_jN~rmA_lA&5{I`JeJ38unV_E8n?9(y%>z%mj|J+vtKT# zriA-2wkQj6@1ocsxq&kgvPO%B){sE*GTD?p;*tfrx%+yQk7T_md)P#V%8DkTf|lI& zo}@ma9>d~O(F}$moMaWfdU3Pn->$cm+7&hc(HaHM~N zL>7FXivtYGP;mS6Isd;8wiI(~W+m;r2Z%^k7Q%oR*-=tV1?g1n@fNfDHU#j1zSXpN zJ)Qq&_8S&z?94*Yf>Htl|Gx|S4GjkeEHFudZW=xG=tGWS3TbsS2vyt&8E=W#)BQikZ4w3oD@jgtbjdtBuVRI(EJ7u$BmAit?#WT8JUZVzG6AusFwtlDDWcgRH@_Q0vc*MyJwr71^k-6uJKrd8Zc*Xs5FHy`uV z=s=q-vI0vc$OcJBA3tnZLa0eoL}UgGEzq z9wLcNIiVbk3m4m^&2^W_1y)iGmjmwj8A(_$kxP~3w-O6RISSbskk}_`DwJF3`%$Rb zyM_|ob87_^(o@pOs1W-TuS`KvWy3RfuqXp%auoSwn20Ix3Y2>*xOJX*K^Lv|ca_#O zw$N5D%x@ed>-ZF;zm@@mbZR-(+eo0Ma1^$;@z7$_v8trl-AXaELqyi0)d}}%QnYZ-81Q2x=N z3I4xZ`2Ur8)U)<`J)BuiqaRB8|71F;Af*HDtdGn81V7Ryu#KmVo9>VL35EqLF@IqU z5j3qtS>GY4?|qO0k3z2N)LAB+)%568iY%E=7IeL>iw1~yjOje|K@}j(c`RUUU}B0* zTaUh~;I2a72qi=;B{248+3H`Zge1MGb(KY{T8B5*kLZwIkrwYByAzSr?BY`znzdZO z+p?=UM#u|FqwhRYIpDdp@xd3VD;3)c=eB)3i84}2_CbZ`mTtRemvudRjD*WJb`K{l ze&;H7?wd+^oQUQ`s%8*D>&fhlMl#6he1z%T&gybU&*R?H{TETS-)417KB zKnYLZ(|+n7IDLQdr>K4J5svq~hRE>y$nL zlLg0*tCkJZi{)yaW?N=IU{K0x{jI;Xy1sMdX}ZH}ArX!4&TjYR?OZ>l5@wD-d46mC zGgzp68DD(kl%yk43s}1$%+mh1z%^V0j=w`6RB;6%ij>u9X@kLTI~7X=Fylk&3ENIU zpx?VGxKO?eL!q>|WW^@mMJKo+kG7a83pS$Y_)qT%`KXpn3-A&iglCx~m>!2{cIY93 zD{z6yVu43LY^_L@t(Db)E4gDgqsL01W7fM$BDe1u4Uy-h5sJAHZc%l`w)q!?evuN~ z8%|TyM8X?0pi{$O(u!B=OdTyYYp|d=I2Q*hW~l&yOp8zt(KwgzLDLxle}!e!{~{Ac zm<2mYUMh6pQ$*1nOR9D_&LBjLq@)IU6iuiQPgm3wVR2jt{x^~}sG~fnv_N_2sfgEV zSiaFV%go%02_}BthHXEQ%4X~n0;DlVDiQnnWbpH(q5wdYEmBJ|!N{aBeCOP+bgfH%Bz=r?(`G0*SOG?<@33SPLN2tVNB8NR@t@&Qy)MS@&ik`Kf6_c=H?)n&q3hq>aF6JF1s7=38O{+Hj;unEIV5V(czHzER~_G&#o#Io1(9aXZ@y zQ(asar`B2Qv^s6|931LfjTQO~kP1B{PtOwXdD}?g~zH$~ZvRoOnd`T=B)xB(5MfQ=1YY z%9Hz(2y_r~(D1v2ykbaPpMEA=9Lk@1+r#9=LO`YSX*t%tyWv32-uS)y#dXtFV`W4@ zy>kzuGR5z)XbwPHqxF7X{r3PkBcHcnyuUlDUxzs{cJ5P`>^%mi0I3rf)h*pm)%IO? z!?+!f13~}sN&wvsphqRv()~Bf5|>7oJxSPh5u~B(s;1)|(9`-FRcqd{>|7X%j^}Du z7^4+nVZc?!r9~|f=y`etFaIkxNnXl(mm712Q{L$fkCBadfNfby!`K1J`cALbzrlBh z+S;WgUB8=XmE(q(>0mc$VtbOTb^cMUP!jk968OMJSqzzi^O8~u4{jd}WuUN_=C%p% z(R4m-79oj57AuJD>aWxk@Myz;TZd(hPg9EH?ppf8?qV+RMqsod`QaOwIt@h|OCadU zmTi8_i-P<>yqVhT1A*9;iqdRhla9ZW8zQU}T^%eYt4-S;>BZjhaCEh74F{X@5lxKe z2bpHI^c?e_p3+=;8a$!7d-_Zu7m8mtQcl~pCDM~r>E>xQXoGR2=LN?86WSL*RQI?u zlGfi=NCKytP``|JCy!gE3wEAdlwkA4$;u>>G7+LPFj2aKFd%(9>u1#u5v{^`cro=C zyPP7w1dv;=0%N=6@bjUQzT(9_UszLq&6uS)<)~I%gt5Ik1(sq46kK23 z@YD(xGZX(a9QyP;&ahyD0a)ln79b!XFz7V>6SO-$?jQm6^aMi(oQNz(mE+Il%r~6P z=D6qAdcl~lq{#}P(?v-RK4a$5kmVBhZnj-*ySqzxHKFW;*sm$uigxN8I48J(trk}j!oU=@ z+-KRC%XNL3(sUexuUyv0j|@3uW@$~e^`B6ACI>E6xe*Y9afm91#4!_GxTO&)PJliH zJ>v4i`6Yt!(lsyn+-qQ=^!MSqqpoAX7km9g+9ji`xf#HCTe@4D!C;tM z<6=qY|3X}g6HJ58+1HQ|;sv(~q-ver-;id#CX~A^n}jPQRT59 zhu^ZPJlJdj;TK*nz{T}^#EAG4_M-^UdrZR@Ee)EhUNcvAr+vPjRty}6&|CX-z8@C= zWNm;u>HDSU<}w6kXUegeRIknWjlR~K{w0!&R~~P&S#Z2YnsK&urgyh6Bq=tpA%+); zk#7O|G${eUdMb2D=w4*EQ5@qpHP|KhUnPnb2Ii3HMP0Pw`=Be@phFq~iEfZ^VKT$w z>z{b`-e1S5JTwwe1=xujxK+>}VUFP=C?!CqaB5DyHC1C;l{RP}Bl1;M%7=W_KKQ?^ z@p)SURaCYSRZ?&edZ}>>o6AvzDdg?0<+@cqz;3XL@qN4cz$iA{qP9h)|VGgTTDbWuIa6 zqe~f;2X`Q2Ybou3P<*FkRKo@_Ne&UgXm*IhRD;l-DB;pIqk(v1>Q5QXZJ;~V6n~R7 zuT$1*l6Ve^narDx(Fj%b>t6OTf?Qe*6NpLjzY^=e30l>jxVX53>(H^o)AY zQA1M8PaoM@F?1~I8>0FFXsV8BVZSP?zPH{LU{n835d8w3_>LWkgEIX5iutdY(~o4o zj)HP;kY@}!IUa58+RxTv6?hKN`OeGp9g)~^-OB#%H$fD5iWBsBoingqZL#rlmG-Tj z4`CB;Pt0;SJA6SzUeZpnm#R&S;%mll60@D@)*Z!ctG=NyzaL;p%Un|FN;$*|arOT3%aN-q~q3 z7US@Cd+USbZf{$kmfRDuqCso_UK70dd>w(M)nK_)k!XPbiU0b_Gb8;)%mEpcY$L5_ zww8Jd#plvV7pi=pB}Dn2y7E?8AChKzHMAd%UDT??516dk(p6AO*8xkVQwH(%*4WU1 zJ9;xm;1wilxPuEID$K;akSQAj6X%^HwTL9qm>wx^;Ajpf6*Z^yZSXF?D zD8tm+GZ1^EIV zeUqZs)1p;Iz1`h#D7~Bg0FRQ@|X7A$JF>>7u+u*h6zBqw)b zr=L*$ROsEUw9u?x=Bsg_4sh+|=zDxXssEIe*&+mQ{}&(aSS#2yQu%Q9aJqUzrTV!5xT8x!y(44r17^>NJ*TEv z+uLM38Vux%W7akh!U^Vq{jshA{DB1(!mIbdKO()D*Q#EP{hW@ zOt-x>@A7_ODu>x+FtJ68WY4kcA0OUj=6BCoe-4N!@R);%a z?%8vP+p|}T6ik7k3<%hGzX90GZq}QkNZH68Qx!1Zz^(FA^Ug-dG7=Rq;SrYsZ`JWk zUR>Y#_4Tmi8}ZobDOKq4fA;KDdY#h#+3e#pl04nLOv!+0O1-&>6zw*Y{#;h?VY!tm zuaT)Vy*lIHo|cv;&HRwh_}q>j|Jbzmp_UqqU!t{WYwG89s)^@p_m0YMIG4YFwKe*jx%F}B;wNd0E`ztTT zLOD1QKa$G$VJIM+BZjGnT*qLPB=Oa_#_$Z zOpz1cPx{OrW%@OC>tJ>;Tu1oh@%XXB_%$FH?eo$>`m)J-A7dZ==gOvN=S|-8U^vqk z9W;}A@w_jngfbTkU`QkU+CwzDzj?m)*$sg7y^Au~^aI2|Y`O0Mx|ldZL53k%Y%co% zno@}_-=QNL-;3*=^UGz<*BCf+!B57uyORN{BMWkU^wJ6_kpx0Y00#^aWl}gVh{&`p z^0BS-oliq9IJ7@hm|miJ?=%7f-F=pzva;G3<+g5!Cyj7t*;)a;Fia=Sv3!dq=Mp? z`m`Wu3!FpA$ykLieHXU06uU^woQ_N_qv*ay`TLyk`}L6zjs6rsmAZr!Bh&OTVL@x4 zlhHGx1%Gk5z_d#xYnEOg26m>7c++GfCCs_4SBsj7`2_J_LnsN5*d0$M;~j@ z)$Lx)c2m)nQ55p!kLhF(M0kW%AWFqAKH8nPa>c)iabf9vc=P{E{3!PvCTdHB=^Gtgj6{maCZ;nZo?liOxM9P!X>^{FT%*U`Oys`mKTp(J zQ0C6Sw67J4Ln37o$50grx_hr#NOA=)3r1@0PeEiZF3&3-Zdi1{3i z(?Gyg<4KfLGFLTZ&`sLJYK&%4Zi}V*w`__XP*a~!zbX8Q0;|A3&Pl>1U;sQ9>-8m_ zp3a^lvEz3K98$@jtHG1IhE^xjfQ#%P+55Wf1n}Q3W$5k=NLofjcccIXXkQ;aTVGi| ztmaOcM)$Vej2k`d1M4BEC^=}D{sQi;CblcZOC1W#%+xDru0J4F_8F!0#}-PgZI1F2 zgP~+O|ER$|Sl;y#?z$4#lq+UnTSYijxe@pzTmp9s1O8qG$X|A0S7~gEu%^6lTlP*r z%~t<%Qbk#B7ET@XWz7QuFWtYw$g#Q)DR<VgoIWEAhwTG9! z;AS5b>433v!Fm+C9*9q|=y%0}K`V*LZb)}Qp^+Ea09_VguFaq-v)2{k79^bu&Bx`L z%fJQgBU02!(_Kccs;IOj3bC#GQ|Bc{Pmw@IM&|Vr%%uW`|F0YwF?{8d3t#gNRwRk@ zMTpc`42vW3l+`siG~8Ppo0CbvoTvDtVUrdF4)nymsV0t^lxlNrF`VK(G;7V<7&vW70RcGp{$gXqR05HYj z0r6`_y@yzwQnz>C`M23ENK~b@n&7}_E%|*)UWb4FRFQ84GTV@j7r_WrO97sP{b3=E z?|FY^XeSwPrtZIHi=20>nCsYE0F=XV2TEv*mKA9RL#lVyf}QstzxU_H+WU>Egqzur zvC-W=z$ShE1|Up!?!i9=0544)}#_Xdx>e9}%G~r0)|H`>sC8+6M zhxO6WpL-MhdU#HuwYj<)a|5wQ$-9V@j%yT)9o|Dj1D?OfLSk7~w&|7X-CK z>4}X`rQEigjj)qnXEeJq74M$_Re)BZCV8f^1{u5Rm0n3ZOL{9zrC4I468l67 zyNMZ7pJ317@P^0^u}>KsFS#lR;(f(>x&=`MZgKGztYl~^=l?_0J2%GNz0vxyZDV4i zF&a*sGh7f}i;2SH9;5uyl>oOf5j=-A4gfV(`( zkVr)DkY=TNlb!Vz+g=~Hz5&a?ie@SO%rze8KUuAxqep54%%WYH{q&0{M$;Jw3PPnxl<%2>FWDF2cK5h98yg#D(`P;RGdZ(f zCGKyRkHE-<9Li#8hzT4f`kr$=LASqKAO5I)aY~mP34(Xk7q^}-Vv(BIb*?|fv6XvJ z;~&=vA|d*eUY3y=;5r|Og|-<)@#%UWC4=;K`^_3kdrR%k%Uj+4A#M8TY}om>)VWss zwh56|i^+57k6HSH00h_sY`pCp<;~)E5VUHky1WVEI2p|DCka?_ToDG$+AS9pZax1% z)us#EYtt$QCaj)&{O>M4y-W!F%vmzq%#J;Vf7Mnj$`YC7);A~d(;_(Ln;z0ZS8kvm zEu!DlF-0GyBGmEWb24wz7^NksMYcG}Oa!ZAh!a3U`#dDlCoW)GwF8?IgDvbYtFdc+BnNRb?_xKb@TYEa zcACS40swj`E{?$RmzS{rQ-QMba(Ij?9%^K9`tcfu>C%DX&`=JWA6GgNEf;i9orfX~ zEoYx54GQ&j|ZfqlvsW(M}O6n zLJbQ0^es>UIVicrT|#OsCk)yUbT|Bq$WhHP3Gs+O;$DkWkT!5=b-i3i^*#|x1!>`K zZvV!{#?-<8JKyFFKWDNK(gX{w`h{n!dK7Z^dvd^be`B!FiPYTP2C|t-j=C4e#{1XI z4pQtkimhmRUq|=cNJwrWS0R{RyoY>{r5o{F8wZ`O==r=PgB)<#oK`LB2o^R)DC)kj zH-@7ALD73rHC4U*=a(j2Uc8cBp1TWEtt%&2um>T94*Z4G3%^~RaowU~eFFdbs#Lc8 zdvxjjxao@aJ*|ziB~X|dzWJ)Pr@6W{_4>;5>hz+&cbeqG z)z#iCY?B*qF|bCqRgG1D0AS#E8O;Qpk3~3OYlD~{!Wflzs_sb_3w^RG)w0XKb_J@{ zp!LFfgFW&Fk@#kPs5{Rp)~C{&pIqHJ4+nGN6jV6y<$pQ8MhL4*Eh@i4oNQ)w@$KMx z)+s^p@$+ji^_~=hPHoG-p58_~>SG-b%8gG}-?d^6j1ru2nwS#6Q{3wC?nA$vY$*wX zV5mbxJSB%fZaZcoue|$McFP@-)KvV9M12u*PVhLC0p)xVv^rM;&fZYE{%x};#@HYo z4kQZt83s7Qm0q@{C!`3B&^WfTByfzCNqmlDaR#VO5JB7AbSFOQVS1SmocVRLk@+{(Y%r6_KPT=D4i5s)xV%1$HuG z`Ny~DA^Q*492`Dkb7z=j4U9U0j-w2#@6Y|HPY(}8^^pIK3K@G=n@(Ufy62kW@=||q|E~j~-&%%TU*gVuT;f?74TD%`)Cyrw6t6I< zR}X6@7_RR3n%3ZsM(jt3D&vCv&#K>O`;)~mSGjGW3W9RMZ1hpl(Ih-|UpxJfhmNPH zHM_OD^7fUA@E?F0RLn$h#%7ISfWx89RAeMvUesQS=fE_Scdw@_8F=k zQ36fX*Yu)Ir_mJeCGHj-swL}@Qa|nMu|IG{h;4(%D7}x|!xQffD(MsS#P|v3 zJ?1cQKeHxZ;MelP-Mg1e8Vf`ND0#BWqJqJ7auyGAEc}Y#6_Z*X9;ar0TxqVX4cKaN zo;yZkDLip7;0~Rh-Ac?u0Ucy+|FSx@!-q$VNx&=9bp~4lY}MYWmyJB@!_#oZc!jD}0j@{}Ieq zi2n4Q%{cEYS=HFeV`9*#dA~3dilkzcKuEOwQzHMV5CgLQ5%D^j^antL0+Ye(S-ehp z_aGB9$mfhPh9FQI+*Q$ z+Ir6}o6nN`6#;&1_-Qy)lEacXA?>~T?w)K# z)agtmb7!9f(x_EBEHLbv=?0z5tjQ4MdUSQ8N({2RH4{R^w zOMJUo#ok}9x3g+-mtPh_r4~MZf6=q^QSqKW^h$C)Q>|95NS=h_S2KrMajo_VTLgC` zJrH3^(KhU|hk%%BQ#M5>RNhi65Re>qirk=th1Ll(TEzlDXApOcfk6^Oz6&DazK(A| zBQdJHn_g-B^nM)uerHA6a~>F(!hAUU3oNbjjoa)mXi<=VsntzTT9BH4djV##4g(=t z0U4Y#(nBOl7)Fy3!betaX!IsNB6TCMbk1V{RvEg6V%kj&@nI6i(LN3*@{S5n0iS8erA;2HIRR*iN;}0!7kv_*%aBG$M`^Rco4?d^vmpgRSPP3xX z7D!w*fru_!UgAG&iBvI*TAw=w0kA5qCQD&?%DV$Zu5Z`0y=v(eP^vEkAQS^p;1Ct9 zvE&5=okN0@Eg(!X8ZFik%+msV%o|Ly$QPplEmbkU-o8+h{Xywn znC-96z4sRm|IeL%>yLkpq#qvY3m-_f%Gh8ufhfOpBlC-)1ck>At5Vx7Z0HYxpRKz6!nWayTB^(7-0iL zrk3lxhADe1`$Ph)Tut+$|04koolDQF3F>c{#p&{vR zcP>Ve7b#Y3Qcz09k2#*QQC8%AjnlR=Xt=*-a(+u`ntb(3JY}}4%U0&;+cS#hEI1pF z`}l1-&2BEOE>BHv|61JsO}CvLY1#eu9kt$cBJArqCj}W%dY+nVi3QwQ!%Fae|NdP^ z|N2lW)D1$*%*-fEJ=WK=U0|bslld>iZL`(kqPf)6+L|g;e4Ok1S^IfbTL(@uZ}(kZ zgi*5&j14q6+g)ES#WDd^V@U}*zH;JANr!ssx%hNhq8CRc&JUHVwQCF^$K4jy*sA%C zm~tL_Izid3c?0VIK0soi=Vu4!!B2^u_l}E(Nl=5MZIuI)2%hh2sc%Gbt3n1?S+!>U zo}16H8*}%AvTWVd$L8^Ow;rE8R6_%-?S~TX;I6tie>=OcQw@oj%h{iB+JP!fi4s)- zIlPD(y@x9rn~tI4;D3yrYtrnau6mE{d>)p11zdML^aUKE7vEa_oZAlu$(o={*VlH8 zG#PQ$UxEu(4R4b<^u8KDJtAv+FOAQ)oy}M=OGd}cx0yeW2b&`%X;^y3VPh6Q3Vhj!de9H3zNFkF>Q&#HS`?NcYd>4W3}B z`+OJE!B608G+p_n(aoUaVZ0pcUG&KDD zWKNJrL|qAG$x`+S50N%D8}99iFl=P@oGBR%P+vu3FIUD`ZRPlB1u|ggd+3*-WyHbX1idxe$k)! zSM8e;P95ons1Y@MdIx+$s~?JpqY25^#PGT?QV6z@Xp!xyg&q|7GCJaX`K4=1Tj815`rbpK2OL|ncV8|Ski!G z?6N^dD4r6CX;k9>GGK?0^9dmz{~&83nk|5tU`xE}pZ!LDTB3 z=HLRg6jhm4Fo~{w4ReKv7#u+Q`BB(QOj&F-;J|-p^nR_gA^=!HwkmpNWB942hw$Qv ztTIKc!EI<0CxSxkf1B}#ZI&CQ6jHTKPqo&(vy3KUxFts63cFLke*?&PW!rT-g6jJ` zsM%M?vaybKBk|F*moFBY$Ljm}{`Yxc%BvXnP~Q7FGZjX?!AIJKHlh4-yC8&5R8cQmU_vA%tg zruFepp5s)h21J!h^br3G4$OPKNLn^r?llVCy0YoQXE4Kg)RECgi(Ov;EC2GT zwM62s;u&^6qyF4=t*=k9(3SG|fXtUlMd()rQaAX z=aQJVlX#qcc-tz?_W4)t8r9P{TtgEWY)selhLyhqwQJMsaF1lL92J4)l+YyF%Awdk zh3aPQ*y{Cf0i+~*Ve3Qtz7eYFj341G9@B~5S! z024%fwWLN80luB4Y<;NJt)-Gc#f88aEtjC#W!&eRssVnqd$K1gaBWjPHwRRx#v%ZPj3 zYd@P49lS~j%lut96@e!UC~rrO<8sQ82_=T-0>_8kEJ(#kv-{@EPzV;>de^&f-AJn< zTdDgG7*gkN(n|`7?84bn`9Ctk`A+KHrNNbP>k&NQ%(oe|C9NT&t(zJsb75KwSk3B6 zCwUvb;C_`2WFvJ%zNI&5sD>|S60Zk^!QU7$g{Bl`4P!`)P}M{;E2AwQh`9cm(n^On zHpDUz#Q*>n!k|CWWE#npK9DPMC_T)@SZDdk`r7i@Odz_rBQzN_9VP4;#QM z12jA-`bM&H4IP#C$h5)Y_u$vMbN~m=WA}*w)ya-L5HA?8S@axy>qB$Ka5r7n*12sz zhzw$>ynT28Ic^rqRnAvxzwT0dqo$bvqj#X-%H?XkF=$x}iY8v2oduw@7_KIO#wdkp zP}u%xx~zC>Ru$#C|0wQSa@e~_dtMBfr718dIEvW)v1L_rodaPfF?^!yG|lO@<$}B1 zrm1G67s+{*`>~cQ-!=lxs+ndCvOsDzTVkgavf|_6X*QY9*Bg(rX0Y5#$v;bs?tA9& zb~uT7Q2Mwkp${|2q40_*T1ZIFMy*vYPj{T$#o?BWp6Ige;$5zH8IFdGWGbagjCA>* zgVly;^FMQ4Jqp#1Gg@DtWwQadqxhglX&*HGd{GApfnP}lD?JzPbYN|Rsq_FrRiq?! z7rt=08D)rp=L4K1a%^a+X`hfEzuve^d*3ae62CG8-(-8fN9aJ*R;cLmvcvq6{&N2f}xr=A!?Yg1K@Z|RkgHqOpC@0kKK*j zp6+K&h2HoS=NYem6W{`Paz%1jO%?q5#nwi0fBk)LU5ou>FGJ{WP|`4?_J6SEs6*_)f1VDg{WP^ri8g8mvz&{kl6wUI#n6GG zAxZ^3j7Tmo^@=F$9I`b`Hq0-Npu|q%;ZYUH_@!FuUlpzbnXiZo8cMle%27N;NO8qaXLZm=V6kHA;QxM9A+mNcl^Wy#~pI+il<2y?1 zW)L0e%_3oVe>DAHuWr5BY!3WN8%#tV7$@x@R0wM*W?R5up0iJWr8Jl%g`2hm5hDW7 zBokX%MBl5&-ajUuPc!=>nV+BksU^0D6wpuV?qkLi_BT=cFqUok3_DXETyC1p(x>A& z##a2YUTB*3s$UJE9c1(f^uH-UJLs6Ugt& zZ>Z5CvxW8hJC4G$ILbJ&*yu)PfCZHNEuYl3Kdmtxlx*DkVKVzYj(EF2woGlJA2OO1 zu@ZTKP%5KL31K?RF*1WLnc7k*tt2(K4oJk8LcE=Q=`P^;th4s<;O+3uCbTmmv5khXsxB1EjlHFfvJL$FXk>!t4eQ6{MoLPPHJ?a?XtU zw|wz1SFHF^@y}#VZ9x1e7%G|HNG2 z8PlWv=ZnWM{v%deRhQ*YoJPemr}jk9{^_Uvpi{NEtF77d@fG(I%*K{f@n9r33Jz3W z8t0IVdlsDGk%Dd-B=*56l0g@WJm-GiEj9M~b1--9SZU@Ilj{~07bu^arZ?CTq3}U2m8b!9C=MzZ5@9};ciM^18no0yU&KiaU zb=e!uetWOTT(*;lmPQ=WJ744>d6+KKnAYEZaCZ0|HG_>AGhbttv)-~>Kd(aL?SBZ( zqWh7SpcH}UfohflyFKaRU3D4)_wX>S>u7QQXBgzO%n<5)-f&t=HN?gG+cEOFTubMu zbP`>V!yq~knQVJ4H#)itE?S-%9v=sk)-LEzp`;lP4aKnVWXt5u%|(K05gMp%?Qc$S zq@%>t2*yI$O`(L}iU8N7hlXqsW`~dGi82qQfOx9GLooaxmKPr5vC?T_4l$vTO5irv z9!+|;_q{@Kn`6iS(?SFlWZFt__}kEm1BQwb#mL$SX=V0|QVyNZsv1qrByT_B&IMdV z2qC*8dYs9CS~`K~)tR=j%Z7w51TEZo?w-1jnihg6_84OfNS~ioY+D=j@GG>w)^Bd) z*=uTpgn~#d*yTjUF=Hc`c*3j+k%~+D-d-R8GI>qRl7DAmt|^TeRn@SsjGm@SHA#53 zzzdJIwd5sEg5jJ7v-DIxjGWSiz$}k18ZwBNNcAK_O$yl$qAGp>CqWZcq?Q?o$$I?Q zkhF_%KLHjLSH!hG_X$46*zZdlafc(kcLfw!7O`x`IYeXqG-mKqZ#`j#*NcE44Qv=0 zX5A-}H#+ch8HV_63wvN)u3%UO<(J^jNMdn1v_UZL!EHh~E_s47?t7X@@aZ??^!`Uo zX1si=DL>~}w`gKm1&^`%2nz+0bn#ts1~S)4h>ltX9-AJ9T`p`j*t!*!;H6Ga9KnzR zWAH=F&XKkiMc;HmH3iruyEyC2or{Mge3}kURgtZqyz3}2Mv)=)cwC?W+wZPuN9f`RU89$oph;Z$?RNlF5Q~p^UPB^0_WYi z=`T+uv&`F|AH(Zs(rF2LmxZr&`1>;BW)uxFKL;M3jzkmw_x2TfTvXXr6+kr5tnE0f z_}6{yt)c4>oNc|`^%5%dzB`C2^nMq$Xqu`QqU63$_!sYPpyuah?;-y}yFdoA;|TcClO8+o)6k!vks$uao=i;1jVYV{_TVM4 zl!lziOFr1aJjVB{eqg=k39Yj$jzdN+kV-q$_c>>YGLTx4@lb}BzXRkeAX%vIeh0%F zfP`)};<-Wl{kORGYK)s9Nv0pFuf0+plfDT$vIY+|t27(W`9y0L<5JvKG{a7>P^T#s za&_ht=Hn zV-uAOPRkBy!(=i4NFgy(kpKbyRh{nL{yXKC5%1pm%V-kDHDp{$Ot_qcZDV_&b8(;{y8uNyer>% zu{sD`$iXYi2yNSTL5>sc29xlRlxvY;j5C(6JC<0`3Lt|M?SIqEn_l^U(@X~Ybs;|_ z$F>x88bwmXjNLZ@zSb>Q9qs+^9}j>2eq6b6Td&SJc4U(%hWx+jGJ8)SNf{Q%+l!}h zg^Dwpk;>oB%GDYbF5dBKdG?QR%jn;jyoa?9!0Cuy8G<8T_lOYA*FYYnL+hd` zvb8MLRDN~72ORDQ1F*9)UTQiFoCZ}us7b!YQ%oWxawq_%7c@V-E9q?F|p)L6zn`l4hothH>5_@(gce)=;oR&m^2D2!=O_9CVgZ82>%48Lq<24 z*{Vb9=&g!Lp88Kr2E6hG{)$LFo8vI+VEEga+LCoxLs`7dJjcEjY!2S`^SBv=KpN6= z@`it?;^8uzik8}sFiJDL1=thaBs?_S_x7Fqtk?c{PcrkI!o`=2q6QaLX_4Z5Lf}}5 zlrB1<>7W!Xib4U!L_zW)vjaN7xf_1xR2>j1X-dxK;Z4@oY*q~BEC+VDqohQH9*8hO z4G$*YN^GJL;0@MAuO(at!#Rka z)YX{St93Zb;PY|*^KYx&PdXoc2Xy*E(XF>T?>`6qy0p3ScoV78o>yT%I63FZMhopm(|)%hgVda z{VH#*zDdxp(LP12W*y+Tl2t&fY(I2CK;FWXS>ZarLm}ND;K>@Y2y%Wm#82q`(}8wZ zwgh%quRrOH4y5SnzI{xvoM);Krh@|_NnGCq1?(3!IP(B6lv-?wp$xzB9&`K8jDU-( zGFGR@@MILnNYt(6ey|S`sXfZH9wWA}A4sA>iuRFR3og+$5DHz*gCseIh%YmC0S#eu z)OEpqmUS*aj0%U@7MfWvG_|*t&2wee0K*0WuZ{)tRcKnRw-bn>3iEialeNLm4C>h$oz7&h#;PUqX~}`I=}6T` zBlq&I`O!Bpd5b?$#W;uiU?RL+S(y~gb--NxIpQxu{duveY#Qya(h?0xM9fWJr2VJ~ z(v+3cHbuaZ9^#!e{G?T|YBI2$;*u*NcNUIvc+=dYbCU3z3x4SMD;N5qWkQ(8d6o1< znXKOY!pN<NicT1D9I%~wJO9O zUBzJI_$Npj5g^*Qv@8*ri8fRv+wAf5f?yL6*Ev+DgoN~jFeK|UxN&kwE}6}3!R|hCP9|DzW{cUjHRdtG*Jk?T{M=a(|i1f zK)Bt5SJ**7`4|o?1Dxn-&gJzsfgaYNkz@Ej=Ke4(*EUJUe|k;t1fjR#e^fSuex|HK z1l0$24zmIJJuTZk_z{cLHX_54W?>2XY`DawX8^?KPi*5N5`_=s9noxMvS_d?@+1 zQtj~bDBr3>@&n^<8${s!+lYA^bREjhZtV@oxugxC5v)X{?fqc4-?urJPw7mY6Zv>c2)|2CspZ_MNtZ@3^ z3Z?!{V&MbP4oO@}3{w`ZAD?Of2oJWU&a&qK`u$@nTeqYn=TU9-Q~t-E_8?2I-1uvc zSk6e%xFL8*cp6K>(ZHPFbKDb%?nyCRoC9B-f?0Y4uXCf=($np)s57S;pE-ty1tYHt zC5=E~0T|e!9EmDyRBfC+(1Pj_VfU{S3vz|#yu=n{XCh-S|#OL487kdy@gYmI4SZq=}c!lApV zuLnCZx4cmK8)_?xp0LlqSjr$jk{0fQy^L{jvfBJvx7Njv;k z4cqES%6e>Mkwq;yM^Q0*(}2(L1~#O`6PVc^L>ZY;`=+vn#d9hOGHnZ)G{joAidi>& zk#;uxjw{=36f{UhF! zfg-0=etcC+Lbc>fK~|z&j7@&U9^#n%7IaY5RkuK>tX_fEu`XPT7T)@uV-UJ^8BK@`18WCbLxDh+ec?Y@GXK zMs4yH0V70I?&_`{Rq@6 z_aszoZ9MLFkCd`;7}RpPprTFPvood^?$6kk2-unP{PtZnY8xW1nFTz@aEw)%m0pG| zsG{(@a5(KEq5KZMgviN)`IXfBp6H3N79|;?rE~2@jIF=;8*?k5Z5ld|$Aan}$`g_Z z{=?kWzc0!SB4t|XY+c&Itf1Z%$Q%$fTedF|52TlWrK4ONNJsm*0teL|0oKIaHLnC? zf|an#8s}(kP*(FRwL;?(!D}u_+EQK7c!OY|%PoZiL6l3}JYmi+TPMR3JlHDWr(Fcp zpu`ey5ZocS4;X)Z6*!_uO}0+~QC%XM^fsfmRUM_IBAooFbgNaXBT~;z2b< zq-eO_OJLl*U8bl?l)7bzUVW%4e~grvL@^s7o^Wnc(OX<>*rA!QD&lBYs>I^H-}+kl zCX^#hnGno4j~IYhk0n!bA3DIhG;V~P#%M^1lO-1{hOQ-p$;H?|T+a6qrt6_E;OO44yFH(LSFKVoOD7r~ES5xv6mGz6abkOc7s zYPQK8{pL~F`#XvSEe23TKp}R%HfVe#EO3CK&Ev7j{i%bh)L#NCqpO(S%pt%ki=^-u zq%-DK(5gYyQk(T~0Vc6YKCXxUrGr{E%Hwa`e*im3+7)I9J~Y=e3wWFf)p>KN)bE(I zA-=_E#~SG&viJYYXxsD^*N^*xWKB+sih1Rf%j7FPLPdTuLKIx@Y@!cjH8Z$e>Q>!z z$KYdhieAd^w!fJ33$W66SI+r>35h`Tw?7)jD8S?o=I_CQCmE#(_lt-M@q}VW5a)1! zOXcOVK>PbU)sB(>@FP8|tmRRzfbfPkD!aocMx0LpH-A{uhqlvTihqt15H^x;kwS-H zHH_SICvRpTu+$m$$pd_FKqo?S%xlaARhv-@2oKap67TT@_Q>iSd-??${+Z*f*O#=F z0XuJrQ?aMR(2x%w$0(+L&aakO4w1$bW*B*JDiLr4dz3~Xdk!a55cM{f@uMDR=0#2VZMvG;FVEkz-t^;DCJ!jojt>(4W+MQInJ>#M~BB$c4`Q^RnB7KY#GR> z9|{3f!0CcnniR`;sN!)T9m>BlHxKdoE7!{8GGQdjVBTT02=d8^4{8SY-#8+?0NxA% z)Yx}+E?BaPH2^jxJY%-Eq|u zW!VZDP%LN{g25c<2VF{#F2GWMF~IOIf+nXiWM4DnHIUWO)wSn{v(^LTBkS~fxr=$$ zd5;x*T3zY=d=v{sG2Fs@m_h&l*<+w7BIxYBf9yW^o3s0$!r_rHSnxKUQ_MQRL91ZQ z->)ty&^{>k`|AmJ?~Aj&~Sa=>Hh%OH#L9BWPnbD0I#1}wk+3NJu z3=`BGD7x{M12y|CAQ;pTzCI}_jTM6w8!@Hg3|Hr&Zj1c1DlAX9I$XP%rISMzfvbv! zZyGzYx2A%%`wb+f?P~l+!k4R*ZWg&wj4Klh4yt4aU_9`X=4!hDRryp)*+TLw0B5Ku z9Zj1utSF5oMesU}=2*)l;YD175&~m#fF_jN!hF1628!c}cAqq?B<5G4S;-wdiQnd%DcTd0eBD@J8pL8{p>xY73p7tCR2UI5QjctYsp_8V_Z$DHH2sGM8UE_mz zN4*QL6gUKo2)Ql721^$jp>naZ(-v_U+is16<9I5z07BDvvPp)JBcTDf)EGR%oyIgV zE_BVpg}C9bHXd+T>5&7Jq1xg<*`Ch0(1-?M7Pf5DuyHh~n&%kRBD> z(t$YW@=}5zB&J?Q4K7#uG~m7R|XxCKVWQfnlsl?p<+?Ko}RD3XxEPiqvHV6 zeP1{9$Z!z7^*`-`q!v9`_jW4(Le;^aD z(^#F3ra;%VA$Axs#dEQ2su5GL;DN`Bpah2p$RUIIeziBI_#f&ZaMpCmF>*Yk=ucbkPd zhKTp>An}J0*XR4O5NbgxOd9-T3R5R2{DI4?izhx1Z?;0ZRbMZ_hebFF`ANYx&l$N?6elZOChd2$GfKaX<6tmwyhHmD@1N0Z6oOeoT=H60nUSTm1 zZ%|?bAL_^tu9;g}|2<4=wpm^#$Y%KTI{z2XdnD)>h@>xRYxUGCGbN>Dwye5$bTrTK z-$XQzfh1;wzE&#ufsQO@X#8-uGhWQmuK>pLFB7}oIEbN2a(CKCv&NvSz-0*rZx*Gq z`1oYAD!jgY+&MwuJRD#-`e`y{hr7txzm>d0%m~LW^esr+4@b+oGi83&{#CFbEHo22 zI)EWK<_%$JxtzT2Q5`MmH;k-(1})h2t71geKJ4{z4H?u?scQ=m=?WPQ&ku!SP}NrY z3d$|a`t}Dk#s$75(kkSEws3#AcF@f`g6N>SaC~y0S`&0Tpk(66-@VHMHW4B(Lrxeb z$hNSwy}&9!kmgw3U|B(RM;R-g<9#5uD|(A;Qz@o!<7 z@ae<3C`lY~(|-~*(KTza!MX?o?k`350@MnPX(iM_Er-Ih7>%x+OkE$?uP-a6dtQz3 zMyMe&pyw}~z(!eIA7qq>A?*sn3N`@iZU!^XCO~j9t3;;1G%l_{IVMBfL{V!Ae-3;t z4vUJ6bZ5F6(MCFC5v}km4KdCljFOd-6w;2fn1@|o1q*U<@7orwnut4-LcZe8GGu>X zQ~?T@ek?zVjhIK7=)fxeRAm^+$ThZDG*|;;f-nLSMc)cTYCZvtc2}e8?<&Cw?96zGC2Khz=S#FiP06}(|_1rk0 zcHZmM!TjdqKHDeNdn&BntJE&d_gnaD?0Fo}N|n3kHX5Xu50!k#`29a}i-{6?g;?=t zoXMQ0heHR-zd88@S1C~YiA#}-ajXP5%a%%f5KWFd#KtwQsrd+`?JkiTNN|U7PV!_t zppYpPJG2ZZ)m+AyE<(hE$5m#Ag^&JGK&=?G<>O)fJIaSjj!U9&P)tpbc`yzP4U+e1GatTkF^{zzYqD4427| zV!%KOWft*woO-Ll9Fa9vJ$nds#XSzy+JKFAu#;>FwDY*m1cIJcbaNkJhlVHQtJ0L> zQ-Z%?C7;$Dm;i*m7+xS;9cht;xqaZSD%9!7t!-2=1c z2;)Wn8REo{Xf75WVNQsR#BTAoOq7h*7PtUH!o5CnG=nYPsw=@@0(cr~|6CIOz~y{2 zWoM(+fHp%!7OPss?)*7TIB%>#UA&1V2iCpqgOcAxD_pz+a-Yx$y^Mp{-B^)8sgTf6 zkeYq-UtQL)_dIdU4%&ZbQ8?gCk*>mL%?FhX)%o$10olgx3fx4rN4p%Em^*Xs@84L{ zt4Y?FCCf)3QO2#GlPWqp3-adI3A3{{8ZZhO)rh<1oBZOAR6Cow2(7I4l4Uyw2Oa_i z$eeQFkikYkAP_UngU49DDZ`DeRDYj1qzpcgw@)-0zA+qs%9a5&ol3)E**-K!z&#u- z@3QWb*E?sdpk$j(26gNNCmY{$^*w z>K3mhnK19)8;N7Gox3BEQ{xCG6%v?aRBG;J; zbDc`#%1QFDMX|sCqh zP3h-nUDG~U%}ewXz5bOi-TGt@f$&_KA$n+%RMIUnjWRBI{9xSPO(irf43hsm=pFm6 z&=M#;T3vx-QG5tFguIt7$gDTk$Kyl&{HCh&rLa`x{WKDzguF?w{+Wj;Q%n%^$>+_A zP<4xh3eac;%JC|cXazVzKT1jj_^HOl?90vqJfIxIn4G1`$Oe=SyTgBA6j`{@KgMM^ zpfgi@)S`qZz4KHmgYE-m94SK@c(6kQ1}9gAGr9LsT*mikUHGGT%Ji2cn+ghbON16= zHM)a6C_J$&gOpMVQxl6=ojCt_+m*~8gUpI?71K#&{@<1Vz()=ts6XKa;6ig3NVu_2 z3JdqkY%h$&zEGPI2V%QWQjY2YSZbWfw&2oLM2;4kdGgR*84d)GPm9HBGFH1wp+!-| zd+P0`v^GqwZWd_dQ960DB~Z#!B!EQV`RItj(u^F388UZ$cPFNr~%5)P% zkOuk5dXqs0=UH5mX{BAHKgbzqbjX55B9Ng}X(^?}#OEMsQ}{^F9GOxq!fj^9dHe9> zEPe((KZ-&sw>9%}>d?A+B2pQA9a-#KM3Ija1r%3K20o7bVIn1o{T-LIs8tl_Y=Wb1 zNC1TmsGY_AzQ+iyS!9&?Cf83X&g440TF||QR#yRXm-ldL$SJ$Zm~#H*>(^yPMfH$D z2!mBRe4E3-Yy_MfAzL6egj5KJ-WQX~HHYwo;GQ?cH~MR&)X6(vMbO%p??}nP$OwWf zPoew31s9Y1Qvrw<1f{g4lUpVf$&dZO!DLS1GO-=#8xhm@e99@vHT(n20KddLIPCXf zD;8D{d6D&m-UlDL0vx&gVZ~^ZJB;#K)+)ow@p0&)^JEMt@<@IwgkYZGo%so~AWi!y z<%%J{{Gm`I9x)8yVajI>vb+s~v*Afi37WC+`!0uWje%OObUSBmk{Akgk*)uEs8p=H z*6GH=Wd~r8Wxt7G8DPnJ3sXy*#P4V3T}Ehz~_Y_n!ktu z-}T}ExOwK#t*!{+O#5j`)1UxHJw27OXJR9LNGOs))-f) z*r4=%t5&tNlyanommak|@mRzwsc*UzXj*-`DLm$HF*-JX37s+gGDq7&I2-vb?9JNYNWr>{@{z2+S8Hp@v6bPz#~J+vKnM?7iXy0fa80 zX3Nh>{hLIiz@?Cg@`Tt1Me;`v5}SuO!1-SAH!y5iaqUDoYp{GU7#+kg%2JTD$ zt{i9Lyr^YnlHf&9Bl@@P^Pr&X0XH{j0OzJLI3zk{k}BpFfQE_sTc;+4iSv2FssxjW zsEAIcei<22s+9GVBn7+vv;pPm)TcOkrWA@weXu5mhuVRWyBWD-hp0K#&#(*rV?8Lh@fgBeRF0qD{qp7MS-6Apu$plYZXN z#{m?&Bb4#U@tofr9p)gi1Co67oaB))M=!e93MjcCQ4oI4i{?fiF%pmQqD$!kA@*NNTbqCI5^WOpj!552eV+ai9A$7YMTE5#w=8lZE_ZnK(N(e8%Sd z?ZKMf#z1fux1Nk=;{AU}ItT7J+pZ1Awrx9&ZQHh;# zn9m`c3#eDf8Ia&*$~P+<=oN!zBZi55StM(-w=m`Urc!Q+3-^lB_G@O)5m=XsW;QkW5No>DB8)l}c+T40@f6Mb(M-#qTyjYqV>4&Y$Y1Yq29Pv*T z_U^8ESZHWd81s#Cb`3&A@NOx0uY&q}fp8oK>Xc|CM@=D}`#4B9c9 zh=*##++d9A%b z&*#uof(v;l6e6Nx8Z(iE_OH}rm&ZCysU_S`D@UJeDu8Qn#DHEErP1VJVapncnATXtj}@ z&&|yFc@HUjibC;s)M4^nNw_$=hJ?hSk_cqQ=mAwFIx(JCIZN{x zRAwn?SgEJ0WAn8>)SB0NL@c$ZYMfU$iOO> z5H>99SnV0hsEpBNkycN~hNIic1_ zVyXJ|Aa;g@3vX1p3o@#e4;UkYLO_PydWg1#;ee0C(9yb@Fw7s_-ejn7S?Ec%89!`D z9}PxBEW${(-)bWs4}_{xOJVwM6v9{W>~w6WZ=N_24BzTeRGh;MOwEr+)CHP(4)jh3 zibE1aY1JW1$~Ih7MYF1$HfRl6q1z9c;@YSV%P*`U8Xzop@JhArh-)WN(rHozB^Fk3T+yX1okSNUJKJ2eUoyz&H z7|!)*$`oEG3?%ZTtj?*UO*Kv(woj{=7l`R>@@+Hb$cg3=t}iCvz@Eyc=#T>=MLqeEC?{rNxc?*% z9f<0=lES@O67|Ilkk?Rg%h8l;kH~5oi7JR4q?ebP*y2TBUOHdlNqioP|I{WVA__Q2 zl}YJaGAT?AiBqp&Jy|_ef%Ev*ML#NC#M@A%+6~Gm}J+HUFILd&U@+?()s%z)C zYE4MZWZdI(xzTxXaY0N>484J0#f}2R@j&T zXA2Aru{+0oIrjVU#wnEne zn~@9N7SiV6(G;{#<-t}v@ZiT2a~Z}{-7Kv`Y$V2`B_jdk4XUH2)`_^oEl1%Io0&;9 zT*in6x2>&JN@AXXteZlj*A=mYAjeY!sanmJeT!0~V>`0pzO7Bg#$(hwLt`DBQV}V- z=AGdv2*Sq#xnIEf9CZHz=*lu~m(;Tt5`0N0`PA58zMVr}{#|%f+(a{^_dKVt?aYwm%!;1prw*%XHPsEdYX3^e&u_AEGvyCK+c>Q2#`B_{)mU25@6*s8 zjXJ|XR}cRbY1c+w2D9Xe!C}`MrW;@+BaDVK*;(fNZmQE+xZX&Q?;Q?{;o9^M|9GcMGX%*}HPL0y0ZQzk z)lh;PA|A#KN|qqzBeWmZ&WlKeC@F;c69ymB+n1a5VWX#{{A=9`b-XzQOr5n-JG1~4 zTKfQc%^Zt$;pr5<5*PdIeGRQ@d8)@cp&$Kh9Y2vr%mV6izvtdAj=RDPb8r`r_DKa} zT#ce^z_yfF0Gf|mKMYz5lC9nfC#91d^xq6!%l7LPoauD1mtqsV{d9kYJVivC<$P(D zWYUVf`_RzB?*PsgB#}puJc@tJm=s}QNs9=2m%7WB<^!H}d40XqAPOV{ApuZ6%13il za?SA#Umoq&bz2Y2xPlKt3$~@X}&@ zKAH`>Qo^2j3|>z>h4){CtPAG0^r;l1;N}lJ*~#n#qHm0L>R9o%RISxj+czD|JKt6+ zdrSA4t#h?0n6<11n38j=$W~+#<7^KpJ?HpIGMok@#G@d*?V;8CE-BPBZD@FtgOiRO z2Xd@jvtE6F@1z<}F&AR6o7gAl0fEEFeG-<`a1sPhjH)(UT z_v`Tz)8G4aZxj=4lfa)lzdbRjkm$tG*?|>_$@rz=Yqba$q`a#KRnbESrHqom zpdE(7dihaBx}=-d7#8wHp>sjptGAu+cp2w7pX)gJ&+!BsknjSiDcfioL}1S`v@3>d znKHjL%?3V+CTX{(%5y`wk6z`BLvnl{>v0@y1LPXCDwu#ss@Z?_avx@<`2AYMWmcB+ zc2C*xJtgf^^VIE1aiA9gz;}lGy5CNTwN+}I_|)k)OQisD<|*G&h+1KxLqnzFZFPcJ zlN60V{9P@VpN?Z2jw_{(re+{OX(otXB+IMB7CiJP7U{StZUGQsxW*`dIT__(nA4HL zm35I~04|7o#RmiBTd1Fs0sbatKa2BC-^kDi_w(%9+_<9s8x!Y1RXZ7RvIhxxg0`ep zCN~=C2l!=|Wh9eEy3fE-7L^&IxaM97q>F5U6am#dJc=wPwC?)&8lk8yX!DR3Gn5ap zVo?@O*S~B+DW^SH5ZHmQ$$_n5VxB>U{CQ9MHtSnX#g%0Hn{2!v7;9RQ*lVsTVLVu! zAz!GWx#LO?b6U5QC%t_7dJqZ`IVG){IA%U(Ax>VrhRiP$zvryo#zh8;rFwO~!(Y}v z3HCD&1-NW9tl@T2IHm^)D{L*QxY!7>wBVnxv7WiWQYbg4GJv8M)9hbPP+ z*x;4vJ3Xx^iM)*AHrq+soKcUu&d1N5{HRLl@W!DhM3^`$g)8*_;}sU!b~@$B^DvW3 zQbxn+g4?)Y&F@Kj+?C^MKhjn}UjPp@PC(KCKrqusiz6vI!pW_*+p$#xdgA1Ey0soE zh3O#gwaMDNt&KQZKA{0U*nszCz(N}!j<6-(B+tmr-RtD9qE0`zfv<$u{IcHH`}vEK z-U-$4oj;W3)EMjN>T+u+U3ghe+Jp`~z3f;Ak{zVVoXAwpD9oXX3SSp9}PwOXOEI10mdpqd5s zYHk{T8qwWK&o@>vcClR%QMORg4-?@FIG^|hpDr9OE%$vFJg5YJO)Yc9q&@htvM&U6 z7Hd?5^Xu1Q)^Fqonf9UeWT3Hqkq|>k_^L<#6S+ ziNsFO&bZ%SPW3#cj)$z4VnvfJIZw%yL=xJ z8u;;l3uGXn*CWVss^apA*#-JM+@0Zt{TDC+4PKN!=UMpQf4ud3x~{+elPo&t+Jnbu zffwc&jrblxL~HzctLox%b$(&ubHa6@%OMG>EG&6H@U*5XiUQB34T}7Jg&|@olRWSz z_drIS;k!`xKzx%R$IjuFPeo;fSc3f3h8w>b$dPUA_wc_LG>;2Ox6D)2c7K;fAkI;@ ztWD@@sv&C(T?F$CoJ+%=G-8w)XHLWtoNyUDDfVfeg>JbgZCrvSG;z=%Qr6C<-mK_K zKV4oMZ(grpUa(#J^OPe%>puvJR^u+tjd{fyX+syiK@f8hoq4aYKRxcHZXfBtLZK%~ zZT<>o#vWP>SIIwQS4Nv?V1GwyiRn!vP1lhoy1QDnR;^sV3eLv_UUX~bN^qLxdW!FA z#IT=^ibWC;F_zu%zC&VyErKmx?d7%jGPVwW0CvcgAsG&;QXVq*uq1q?G*FI;n{+f0 zNc4?c-3jC&X!yuR4btNOP;J5G?vanzk3dxtJ%FUDw<;x6jyhYB^64QB8De{2j(ZIH zDc&eh1@Kz#O+j01$(Bx$#TF+%f!<6D&%%-Aw)SBBodX2SZiMbgZ?phz0rgl<1GK(@ zv6btCz#M1-C=-`zEU!aLc1iH&gjRA#tcw(+hNCWMd9?%f^@ZLkN9HD zFB(p_g&S2ZG@|0-Di&?oHF(S#9LgAP7`08Yr^P!i1&t-y{ zK&w|#Fk?yj4S>p&KH$PkJ-6oen1gGqf9EDTH&N*{>N zATB1^t4^}s>*=xn>es1Rm$2Mvaj-aw#JDW(UlTlby%#wgkCXKHl<4V|#hBTwZudOW zT*1@RJ2#JQxbw|~EGw%g2=c#AtbR@d zW6Lm~mL+Xi?&}HgPIl4=Bi%IX5)1j8Uyr=81wge367N1-JU|IB<&MSk9 z$te_^#|hOm)~oSVd2=g@j-2{}^8D(wqa|?ZxBcx9 z_OA6oWWQP5IA+iuQ3u_5@DB-S`CEmN7G1dlPG1!LWdf#6P)Pv~k2mP%Iz{dAh^<=y zy(h)KRV<`~(V1W8?jFBO%z6`>uLd^JS{7W}k0;MdSZ(}^rldj4B>4vj>)`;kvF{MB z(JAklgbB>6w3Z_hxZ1rOzZH6JScH!)1qUT6)P;lWpLb*rfw~o%KOa1Mu359sw)(kT zm10V`i?59b@wQ7NvVYn!v*$bYmPtqu7ZiXf0);#-&fDiN7sLf6A?Ym{1%f$3?+@px z{nKMnVH>jUv5Shc*n@a*P*w190dombFKlME?(0XMpmall+hgrOMgDHu`jk3QHz&md z!{>5MzA?EtgFWU4n3~>K*igF}VwgM=0=8UkeP~okg|fT0vYHg;0kZ5~k0;|nVs0wD zkVNVMyQe2q4jLq8w)D;=MCjKEeYQ9KmFCT?YiM`Y&>G=4ierXyY4!`Z_WDsIdKjGA z!$GMu?knu{68jw{L~YKIIXzl3c58P%FGWHVp0ZABde(KpYP=BKn;wfZRwApjw0se= z1J@Yvh=CcHzxt=(_&kEuh~UyQui=qx)kU1)O_Q`p=4+2^;Jg#1t)kcEwIKmoHehvg z{szAx#9;ymy<_#BpUu4ubX5F#s$F)nypcEF8-^0V326kMcfy0;I)b}xW=ZPPL5hhc zWjN?XANG(#I~5fb8=GSgr0Tu|gyi&W^f#i5sYWi~ec3@(7JBae2V}EzEZ6**^yr!0 zEugSju*~3fvz)u2-q}z^%%!-+^FH&%?((qwLc9+u>1Tr;TUi576!66Um;wtx9_(eo zT;R*+XHfm~cjJ(+wQ=FcN}gVLfppb`?(cSvUs<$4&(U8QS8qyHrmrr8P^F9DT7CP$ z@555r#s(7?(v{=Y&^AP63u{0Q6~TAX&#V4P)-VVUp+hef)Byx1Pa-zXe_&~v1dUxx zQ30n=V%CLEp*KbPMR zk$@`_>}jrwtX3d?gZV?!Un8O1qKrFKP~ITX6BVq^uEPE%QygZ+k$F`{Ej@@|6Wb;Pg6Si z7tLIL8G5-oaC86RdF4QfM4*uj^&u*Ob2Uf_*}d(hF{a=sY0k9@Y;9Q$|u){p2@h^?~f1J z07!^Vq|4T{k^x<{Rm#to+{%}dAN12q;U%rptRX(m1jX|m!xSn~bsWeFJka+8Ay&k4 zv#Yu$WLTT5;=b~gS`h3pU^rpt_WGeopsDD^EBEdcCkW>IsgS>ineQq^X)FGS$?gi+ z>l^i_Tu$X6$OX%9Vx$2uwxlpgsKNj7X~U8bvVTz#{-Li3PlM6&8GDq!t{57~79wl--dU((O`D2T8=_>c?DtO*@)b zP8_i&rYgl~H9gH#CN7%b4E$Q9_AfFzMRk7kF<3Y2Igw+Gq=UH&Pi3ahT6@Bdc%wCT zoF<*~Q>U}blkVFifJpFLyFcc86ag=2fH}}6? z`o^S8*Zr>aUFpDs-NyGSC)9xcmkpG0YwL}gja<^`_V(q*?hh{SkD%Fs4Kh$X);j+$ zUIDKg(B<=a`*RO=``##dvo;UbJ9zDreYjaHOF$WaPvpy-x?#BZ+KK1=qwTTXyjhjW z1FO@e=QeBI<>UJ5K`B{H((H0$4i~R*yPx6-4XO8}Pm=YHVPq7__mev$s5C`<(;}UQ z7wF?#s)lf-ak!;k4o+EJMon`s;P*VU;QhIHo~2@W<~*(4pG3-Q=nDUs_)r+?*If5c zyLx4v;lZyn%gn&1R$WB9;fnO<1yQY&kY1o))9X&^={70D*MaNd?tW%NyQ8OxnYDK9 z#rDQ${8yGxKY73^+`f)yp-uz^CplY|JaY=FJysdcFy;Mn*47U|+tqZ#7uZwB8so|t zEeNjq{NC)3Vig?R@-{y|W1~Rq&lpL$Jd`RgFhrtuy|Hri+*)$^)?Z6F#97Ibgo+Y| zLc-VO|5fs{yC~Rt{PN@N$#Zqw`ftA8`RQ)N$;q)nP2J)7TvEOMp5)NMo(IC$Zh+WS ztaafY*qtx@z~nhSvq@pohK)Q&<9QbnZYbR!T)4@&)RHQ+8cueGWOqq_slMB~_0}%g@oN+^E*4pd!^ae~mJ$S> zJt8(^Bei){dP@oPV*RCn)G7De1#nP6&MkRYvUupABAwD@fFPF|GsNgm*YiKf99=kB4NUqJnC_@%y_)g}PW4s`|fmlSs(8sN*Mc;N4Ttu@I=vdJx5H zlYd#eTPOWmIj-$w?X-hn1?LB!l*Le%;d25VIF8THC006{8yxO4_jrlpfCRM)!yxCk zESLAgV1~p*vT26`)KfX*eV^=-$_DQ=c(L?+ALB&i%{(7W7#|US&-c07I_Jv*KG@YM z)0brCg@!iCI~6 z(YR#amCHuxompwyPSI#5ZMP2Q2#$r~ze`+K;P}|jb#lXRF4{UOtS<0I z9M6HWGF~?0kJg{S9vh+^U+kltDnllvnn?J3inMU>l#m`ZkKE#0IWq|kdMPRzc{X&5 z+yUE)quA-vfM&Op_2CFxZJo7Cs2eX5^B7NZCEIJW>7{{Zc_)XP-{M0gx(5mo101S9 zCnygNP^O_9@qosPr&LF-VxA!OsxWcOutzQALJ5`=?FM{^6X16WvbSKvKX)wwij`@R zKX8!RDf=ke^9br%_QjgmC@=BqDRnKXBn7-EFa!nhT$t)4)Krm)ZqYxS|65q%-kL6aoh6Ex5}`yN zW{5kU4ZkICUFxW3g(d-`*VBx?mz<5tet|!zpH%(b($eyHreJ~sOcQo)y3SgWy{LIZ zMyI?XXqa%z_3kgv><^A;T7{A8o8j3F@7cb;$w#x7iv_JcL_5U}SxNBb7Q(cRS@H#& zy{&xG9AUP82@$PCWfsw>@An2KfcLfV9dcyqrGa1wD?H3?LA{1Zm{84dDR>t-rVe)2 znkHYOB27hXyb!4O&)1Pv7HwfRTfdI3?jJP z)1XG}^jIT&?N*htH7%1Mr-!lbmdhXGQ*A%Pmu+C`l1b}6?^Ry}-W0Y0IyI| zw}e}v#wM`NOfSp@O{!c)h=a&Gl8^SPea^~n3tGbi@Sb}kvEzk4w`;NKI)*`dDk`jv zaz?#?A%)h9oFC)7BKm07M)`~xJ0e@5X=v9QdkN?kru=!gvK5x4(gA91Jc%bQLev<^i3_w^x5{lSqnAxDXs1>Gckm@#Q~hI=ih& z*$~hlN4(l&z}^2AD!6)V=Sx*_)atTO9fK@gzbyZXZIfqU3CIWi0a8Sb8|1noi1)q<$wgOO7Hdsafoj#;LKo zj0CYdtZ;MsSWcdLa#W7Xf`FmMn}fu~{HcftXg-(l)b||xy6AOK8x>3qmk<4Nha+gzAkT<<1YsB7FM6_!saU;WaLNp z*80&GKDwtz2NB?WByO5|`KgI9+4s6iVr9-3f>gM7&E^gOr3%+}c6RK5aGg}Q< zQipZIBwIQaJ@O)TRq|azH!nMU3N6)#prZr1h701Og z@v6=SdV+l9|5@t*)65siUV(4a8J!SWO1>+zr62EiD1pmiUPd#N?E0tv;8zr%hx*YX4$3X2T(I~8pUF~l+p8FoG(}fod|gTKWbYwJ(6;z$j^lnj#YNb2 zg0e_~_1`+JDxkiYP#`C!D016VnG#>gzxU2CASX=fRoKpr_>bn)X6+#CG?$#*N@Mr) ziwnRV8tIft<1kVUL8ejIa02n8>gwYZQ*;ycYK{!mbDrE+ng)G1WEFi?F)Zf5#lp<0 z@svhzS=H8X{AB;V{H?Y%YwqZGc*%WiATh4Y{eHg2evUWzoEt@4cDU$cCi-MC3STS| zn&;eglIzeg85|88l4xoN97+9|oJ=I7d&tUiOUucS`Ry^T%|7yN#J)F7h|=VqTkf9C)!`Ao1@kf1*!RRHVzI|L5vMJ|&XE^2hO5wjgEiPnJb@rf&y#W^;9^SM7mvnzYp=+YdZO;o@q0sWq<$i z8+o-H2MWmUq2qa&c*yADa#qxDQT@2>oqU1+lX(BLN!V~dPobIPepo^iX-Ejcax23A z$m`c;+b>N2(a2G^@8ghws4J`Pq6wH}>#{XQacXC8bXS{Dk-qI3T=-w+>Vo!^aDtJ| zeA2cB>e=nsEp^+Oo6kXg9cVgPsi`9WzTNNdU(ik^!0KxzD3;!&-DL3a@OAIZ`(sUU z8siW6LQz$!*ANN2c>qUl>Kb|=o?wFhw7F<|ow|CN8u&1TvyX|u<`JPMV@QwL<={*i zOr&cr>e6b1c|s5#HMJs}u|WIa0HMh~2WqyRoF!%h>~Pk~T`Q^V!zkylhckArW&es7 z}a1q z)E6xagVjY3)_|4VH3B1&$U4)El~h@jv4xqr?C`=vE^9gE2vnBQ^qCz;B=%!IlYo#y z$n`K)6Hus}ihkUuTAWTcDLx#UtOK-6Org@O%QV)r0F`~@Z~Bc>#wvUHc&bu0~}x zmwc9-KhsdMn}JYPU6{C`{J6`W$?4$z!mh#l3&IIx$yb0dkN)e)cx==}NG-T)6Qydp z8}1GbHl5Ct>XsnybVtw2Uzf|PtByH5LV}IErSQC=6vH^QnvRt9@2dleoia$otiSpH`+AKt?Dm32l7%kPQj{7Tn>EHJV7hB&-H}`s zhhQUidhjK6#NFfPndteix3jT;# zL|wN895TM8BZww1m_1juCgaO_wT6|1j);Cz(+?PvbJV(XmykIdhokI*YZ5vDunb|% zoSwZBVe_}%K3%SRDO($L7Pi>9vE}xfnj*4>#H6}`&+_4hVj1$6a=}HeOLemsy_3Ta z5Valh-Q|F;f;{RJdTzrnqz{q^jcE!qq_cZA^A7~i5(IbCfY5!05(8?tZUOmzC(``M zy^%Z3XBg_Ivl>*`k#~CF}!jd+Z;%UJ_O=j{@Hwq39X? z{q$rZNr=1%8*UyoIVgX0S^AyQhuz})#6R(D8HBLrh!rIJ1)J6kWb#q8%y92uhLx8{;fLVA|;X_k?jmFxEI zWf0OtAm1b_hbU?&D#Wt|fkn!eL6f8Y9D?8dXViOdH}ywva0(ey3)aLebt)6>8I7$y zK0#=a7Vo2t2h`v;Vq|q;{AydOi)oM=NH!}!LMCq zSYv;jDJ)VJSqeiwO`}%$&-VzNBTp_X8)~9}X05utMS#uSht(FK7K61dC} z^5^zof|_fEEeiCzo9*-T89ySMl2`%adMg2ov&Fnp>9y79}qg3g~s$Cw#KtAm} zPj#P12WaBz%vM^1g27W?!5iOjFt0q(mtt~*KvKpj! zNw8+t{U>s~EG+$Zee|8DjHYQ7u>JE2UqZwcIc%LKD{mvUl`|i{&)S#n{_4gIf#fo0 zB^xw8{yb{$Wb{H_?fcwVD{Rhv3RPB;OPjcz0ky{hBiC`gw%VI`J5=q~KTRLkI$AE0 zt;dsZgsM^CN224TF0c_f_oInl-b^%IWkuuonBu8QRG~{W?F`rr-j2?>yl)MB?c0=O zqCYp2UJKP4W*x=i>b7y?aYVR#jRK?~Xf@42J#{aU{->_GhI}r6mf4Ix0ARNO5Nh)h zg04?wr{uuK43}momfyb7W7j$LDQiy)Q)ZS}fMmLd*6WYQL~_J&jX$2Zap?!*Y=P_>Wzmggc0Y zF+|<6B37ucRo+n#>YYR%co_@U8ql1@d&xl`@awdBSO$r5U%<;Uo1E6$=hcGL zsJPfAp5MDVNvpoon9|2;;jS28F8tuI>Dwp1(gl&9sl+4(jGH`8pSK>Kk_!d(SrWC| zjG&%NMhU)}ZAPL}tGIZQh!RX|^fK52&Ru0xt_FtUyI!qfQei;S0`Hr>QQaf+=DaO^ zB)%bqWbY82%V+br*HA5xu^w+$7ffkb_+W~$@qGtYQaf|Id!(Y5}sFh?i_yIpoMEMw(1XcYog_^TTo@sr{Ajz3$=FW46pg=LF6Wb~82N zsq-vLo|?~pLddw*^|5Eko6!DAFTuhBf&r;`&4FHv^1=+V+tupkqVU^ z@r}AW`SRHb>9i`KCiiOJ(b%Io?DoQj5as$i=4VVW!O2ZA+l(AoA;d|-ogV%N=3p{Q zXwHGK4Ypx*?i4~l#cfnfCBK*0SP-5O#@X4bR%udSK8=8MC&==YAgaB6qr!^ zc&;T9(8v>X=}3c0;&2T%;$4}Kv?W}4z~;cg{+!VKc0=ZbO$NWwfTHK*U$^H~<&)3J zE&HYibKrWeML|*Yx+js+QK)N;ftjQhEyYWxz(Zo}RIUFQ2NsV7x75bdPK=S;WnLlZ zDVjJj194lgTrrOI9n2wIQj3Z$L~>>dbrk`HVyGD$?bN$7#oh=V7iR&LYMKl=ZIOZ$ z@}Vq{E@)S_BnGWI5Hm&~APgOm=#u;syg zh2OZ3vM7(=?jbf?dkkpt|7`$PI7F(78h*KXet%ZT{Tc9ZIKPV!%o)DzK3o;I#%e!} z_gd(JF!Rt)qXMOulaSB*55VJYIv@AWI%EO$--H`S#O)x4lz^6xVz8?Tgl6iYX7Q66 z;8;X7bmW^u;H2dON9uZwV{G^h!eZ;x>&`FyYAzYH34STFx$O78KWHY)C1NfJ>l>5G zwfl2RMG2Zgv15_KCC;Zd^nHd(G2Xd&OZqHYEG_(6n5ZfG+9@mwdUFULPZI4YZjIun z@~VAKS!_$n<^jfd<}MBH6(Lfy>F%uI=ozC1epI1OIkZhp>@;8Yee%WYV7a>xO3&BX zCx8IJzzd3Q&C(lLr$_kEz5~+{Zy&dkU30sZa)26x6kt!GwasN3$EeTd_h)Zl1+DV! zLo){kk!hpPVZzWGj(4Ah+4o}Ze4!_g$HiHz?iHId2__raSGX)v26L_iBlAxAF!m#e zag!t>#ecE4EoYOZ*LeM$CCBSNB`H6D({vwN=-Ke#IIED5d=3*IiUl6l7NqV`hi4qf z*-l3A)8KN|F&D_MODU!)u=aX@_z(Xnn=*BbJN+I`L6C^uvB+N4xg0o<<~RBiq#cUJ zI%2EQWT;Xe?Jzf<6HNv;bu|(3xSsP|HRST%3Q(R0CGDQ}0kMJkqTu_=I4aN|(SV7U z6TR9OyA<}u48F{N8&!$mH1w~EyA~2mn1Qzp$j2?p)Gk=QNz+xxi{#gC3hJI{E&N3S z=12%yHQrSyvzahy5KF`Xrc7~1pqm+q=)1E58y0n-Yb>k<$+B8QDOs#txSHLTO;|jU z;7GY8jqM-bI`mRbpsniZ$2Jw|HJPlwb*f^IB5z5mSPu zO3R-4TwhER6*biwju4adqw*BYUfPNSY<(Bb%@s$kc_Ps~dTYkct)e7o=#?TiM}%1% zto9$t%-AVYDBMLR(q@`>mYk0W1N=)8H7>3jYboU1Ko|Jx3n&pfv+7iBCIn8?TPd8l z2q_emX#S58Lf5Ys#OLjQznxB}GV{2e_^b-{Jq3Q=uH^{-)?Kl|eGuYlV<@82eyDU8 zx6AL?XXp07h(Evl)Z@Tq8h(bUVFNko|7%SB@CV)4gDs+W`9itO^=Tk#CjKmi4ol?c zq`<9#eng|hzN%^ZCMMfltA^o$6Cj32I449w&sl8n#(k>`D4mB4STZD44!ddLoQ-UMtU$!z%k|&l1@)l`>7w~yg2+6aC7J19@&AgNXx41R)T2oOo{78 zCsk4x+Qpa-j_@(dgz&4jl;lG!oe|;24|5F2Hba>-U$1lHiv6bKaJfg<&YPVYb zLl@+ucf5{+WwRvHBBWv^@)_@VysszaA}W;#*P=nHJ(7feb6uzTpqjcUcBVNZdnHfa z;XrUiVA}`Yc>(<6+{I|{-DqV-#rb`*l@gWYcomg9i=az!uGeu#ctY&iTg~zhM|S-= zgI+9b$N7!RqBX+KQJzcRev$x4%;vrAweGZP68zD&YV1FZ%=4e%F5A#;a$sQKKj`?F z+x8>=O?yZ0ML6mE4oG~r1!|Z~=1GHA;Y|63L4-uHldvci?tJqKH~i^lLYJP<{-i_z zuiv-z7rKTpB9~dn8IeH_GR0J0jzE1rM9IwO>sIxK~*r^qS)1 zxR)+Ne~5x<7dI4W%#gcK>(01dH7IE4k739USsnlbd_c&dwyH~xjL%jE8{sdv6nEmx zt_EGZ6z(Lozva^q`Rnds@pBfMs#_I^L>RP2nV&5t<^2QD{kNY8IyXb(DCc-=1uIix8@m zRP&N#@u-_{s&qr8jHX@S-cpaCCywC95CaVNwdMQBea~l8H)fSA@Y>LVtsEIEg zQ#TH>8w{k;m@NN(3M_H6vozh>`_wtZOI&9;8cf1!UX}WltU!$;DXOeQPi58UY)7#M zAzm+L!(4B2_vtu^{U);>0z;0zw$!$@@{(6tS>o+p&@;Lbq3%ck7I-_{m zZx`WO5j23HCgY$cNF`t}orK;!@}IkiH;wg=cY@;FTq}jCad-wB zsk{mkMz`=0M+!h&`fGCBp465SqU;QuFNiqxZJK5V1QNSc0THTUa16TNBOSldQs1iy z7}Ipa+tdD$$}fulMGQ?c#Qs)-0nV@d!Cd(hE*TQvX21HB-rLhscLZUy9H-R~LsXG) zKQ%)5SKd^zB1s8F&7y{@tw=4IPh74C`PSr*0l@Jknj@Z3r#uWP+^s6DnOyB=klPHW zJK{SvOHec&>1Z2v8_9f&c>AOiBTz15j-mNyq8jZ-W#yN>E7$3d-%aY+ioffQNm8dd zNxny&7PPhL`+KX5zgJvznpASTMn)5~$J6dLCW3FVEB7-#aEgAlpJRu{vvdKST>RHT zCkOej|4u4ks11qG=li3Zou-MK#zMF1^cv$H&R)ZjSL&ToFNwgGUVfUe*&Ski^hv`5Y~*A~|^DaZ_DA^iCd&c;J-8#Q7)4 zYbVrk*jQM=-xCZf3i+m4^GM3brp(Sq`XE)dy8ZXdR2qYs*9_Z}Y5R$$;>NJ)g`&_50g?)U!CTjNYSi62bY4J&?JK~nivS{GGd?sV1 z6R(n^VXI=fNFx8W&4eq?RL}M+@B`$vDr~7<yPOP)xs=c6h8Q(54!$(9c3 zvJ^mxlU!5b4;6_kPDY%lsIWuq4=NLO==S*kpu5dFBQIbiw8J1&>CKu&veR0V+0|Ar zuaNbhoY2W|q1~JP@eW6TIp$8+SwlDI?_GSpP{=>%eV_De54V3H3xmTAE8(z_?VagT4mQ8H=mjZ^+Fw89KE#;h7;H+(Qx4T|KVZV4VXvb^hpRHVY?5F z;#R?2Q~Y&T_!I9bF5jlv9_%$GT0>a9DIxb4MmiWVR4kUZ7xZ6EuAa`Z(jpyMXZOm^ ze|M$@wW~q4*pt}Q@?*Qz;l2@OTL&cux%aAn6Y(C^EY6ovtAy;qs$dj9qv~ z$Gl%^mY_ww%Inx`ab`--4P65jM!4{t-Xsm)?f^?9ZCaI zS-SJn)qze-kJIbEPR11v3*vPo&KE5K>56pNN1@jai8pRif$yg^iT+HQZ?1p$sAFKd z(%8kKvN;gqd-|jfO))&7;4^PWH2LCd&uM`mi&Yz9R!qQ;(;4ZX{2Y!DrX{jg^pE$eF3+;<&v9kTN&OkbavdM0m4kBnRMUWWoLjN64j0~NR}oZG!MxZN1I#h_e2PJ2r!b$*K4i>F-+ zxSay1J1lkjs2y0f88g4S%I-(xdNH}i?7sabixU$ zgN_BZeL>5!a}uZpyx+}VZV&&%-sCf8Pp`>k$?vwj6@mgpm3ht(t-)WPtidnS zPaE#2-03Pp4_GaTxGWpxR?yk-)ko2`^Qc`u#zO;Wr?)6TgOi|Qoims+)c z8dbqCx+O7RfxCZwfi4G=+0M-NLJti{C_wAn-UREa<0!SN!8LgqS@xW$!r4wj|oU4)ijL*<&VOwVTc9C&|$0Xo~L ztj$YuqK**3^j-RE#UQ4&%r2pkHm2lseM3qaQBKXF09%Ez=HPQ1tCiB=$+7jDxLu?YRaFN;k*>_Vt>QxFa$LYw*0`{P_W}RP(9kKD zFp_dZz>6X5$wG==PL?Qt>E*>=X;XVn;ph5Qrb}S6Mm(PIfrC!W<>bTtqOh9Bg^*Bnvn~A?ean!%&DdQ#W3%t-=|CgHku_>&J{*X1>xT%hNNJl~u~q)l@`=w9+p|Kk9n{&~*3nt$>?YIQ z(=l(!jMu!Q_xd;UcVe3{Ha7PB^UwdqU;G6&jp>y^08Nr&nvShP0IF>r-8~C0x%&D$eiNxy!xO;6hcq#`Z4C|%V#K#?+qQZ0 z=3TpX4Gj%Rcgg`hk3L_!cI{0!-E{HA7cW?_z>ma3aRkoT{ac@W_?w^Gzvao&=rH>R zi04Vu;Vh8OP?@A$!Z?8uMK}i1e@AyOM(5f)KE2@8Jw|9%^~A2Dn|?63f6K|^Cy;+` z|H&_W{TG0b{iCiksg1sg+k;3q?C=K^mIGywb<0`RU@do$NLP4a8m43t0di zj9%o5KqCS*q$t9gBA-$-Dp;8$xzW~9z44=MORpMf@BZ#SK3Y6dW@mze8&i%4dfT%r z`wBx1OkMJYHRYqW0MF+5^YxKRsOi(V@zZ4_EIyT)qEr3y% zhK^^CeItAN@3Wy}T%MvG`KVgwOFWoC1b(Q@{K^3)B6Ok(W} zY&;+wC#Nhn$L_2W6?sgOAe5Rqo1D!f_#6iafd%A>Q$kRScOcrh1fINCOdB(h``gz* zfZFo!tFlW`F%w-X$v}vNrPy}LOi*;X)ukw?Nrz~VV+UMDKRVExT$!QFR-v8IO#V6S z+Aii{D>6R70vw&t=Y)VOc8ved0xfQAp6bchLa9O^WAB#GgjSllXhL|?X?PDRq?17P z@aS%ttVO2uWaw` z#lA=80l1{k5ccoikEKrvnzmT!>NDz-4(+P|=md0;KNJKezgGmmY}vB+zW2RYgpGfY zQmvGSj~>|l*iVO!9w?8D*2?^n8}1eLZKxov8Js0Q_Nde+Hy!W|-`wz`r zux$3+`Mhk719b&@6JF_K)Vy81CjQ!zw@F@=u=Dhw!`+WeeXM<8{*F`izuQ}S@K~i{AMtD%PjEk4tM41Btv^wH=x7-x4<4;N5Sa2W zk5wK%UVQ?4$iYf?d)p1Oi|Yo8GmG`MlZUbmKgb^b8h@+6PXHKvqy$|@NrYMl-oH_f z?V@C!N7=2P%KG@|dFzprf4MgsjjObCC7?wzp7J`Yr^a^iG`QxPYp%NLDlmr*9s0+A z{6~Kg0%qF{H{1Z`z<~ol`q7V$963VOh)Sgb6HH3*d-v{r?6Jozzp%mEHE;XXH@)HJ z+4B~0Zk0!~Uwo}G<~U!9B3yiUO6UaSJP3;bO~p8q%>zL}=D2M6WgV9x@u}sLG}aRs z>pO^@U$6%t+ujmlbe-6<*E|5%-5hEL_6Bgy{z(F&t)mthjWF$4Ne1Ra(lFmPpZgHrz?_1Kam^ahVrsN2A}lz>EB$maHn648*#v5Ka>VjvqNGlE0M z8#aZ>p5Q~cXNNXZUGD=p-iP%YrW}BZ^8-;rTHQN!(SRY zv3J7*hj%^0FGMYml}n{Ee?OyA<9DKRtB7Tcrb(`2TW3wkaYFGAr6y-xnDq?IUAFF~ zrB~e8+1vVUMrQ;Dciy~tOP4N1Jckb-KJP1CtYYWRox5Vi3M?mH``Xu{Z?C=fS}br8 z|}b)h+5;r;3+sBl5v#k9HS*y z-n8UZIYwNrmri6y)@KJFA3d^jc$6Pj<8~3N@=Es0$G7_uW^n(eJsl+BQcZ>|q(%Pb zI)%tni>!6|FYN|8_$ZNvJZ}r!E&^7fsT|5LC6!vO(AB?e?M;{8aC2Y(1mDebdb~o% zsQJyPpX}pJkGn)YSWAmPGFHbyR~=n}q^zY1?mo~dYJX^DD?-7VqNkI) z);061J4gRy$LM2&)u2h|kr@Y%5w@Qw=V~d48$h+!%`AL+Mc3_%yB2n3ZG-!>jX%kL z`PJ;mZUuvXJCCN2=>0=M5L&9Xuu*;#DY=V)fByY!=6tl`d(R&F_=dJ&ei-QG!0rFn zX9G7Sfji&t{_gL-@r`c;|MQ>!{Ez?mkNxGSzxkWL`TqC6AIvk)Jo6v_9QqUxFDAK5x{a(%>qXBm=plD0<34e$>N_xK9Ur z&O;L?Si2JZh4{s#Y(g%J<-S%Ge7%JYi7SlL0o!{HUzMBu7XXG70-F@-7Bl$+q3}{J zfzC8Fn&6IQ!*EWzX~+}`eA88-m=!zPiXH8pUG1ISZJk|hUA@Jbvj^Vs(e6vGY3u3< zE^;__?AZPH-+#|N_h7Bl5d;2YSd}6`U<#O->@yZ&b=I!D~d-zB#oH<(OG1aVA zACWPjRC&wANcKIo_8(jTF{-foj90n{Bic+SF=Mq>B;frfvEVQDYX=(m=h^x#EJE> z^(j;*IRCHi%erP{wdxl)9r(R1g^?GyU37bGo7$dt1!0rtcYWcv12-k{`q#hy?z`{y zE%0-n``o?v-fQ{q{oe1r;~nn+^TZQRV7rL3!t#XAe)hBPe)qe<;8gk7|N38Bcwqx) zS*23J$@c&FgWrAo?eFH7qmG`+{`DJJ=937783^K;#ie>+T!gJ4CL|SLpew>vSfo-s?Z9B{oj=K3Lj*|tC`ya$1?z6*Fr$8aErLSFnW zq&~P{s|K+|4zSbEXOg&Mgb?vMgoMPi$Z4(^Y(t+#p)jZ7`oqz&vov}Mt)Z#BsidCU zxU$Sbveu3sBar94HEtJ!t6&nr3`9+`v?&JIH$updL$hToV5e~8Ps>0G{M=B;;ht-2yQZva~u?-;-T{qLVTb*iNeY7~J@W4T<0 zGWbw-VF7(wh+mGc9wtJWfb5JJGj6~AcJCFT913zcx_is^2YzsD@Ak3LVSZkb--%kP zl}mglYPHOXTjg^wFg$@cquIsk<{)V4TxD6(g!cAjSKM^tou65B>D589n z438c-e0*fAT)}CK!OHhoei&P(f6O+ETa=o>Q;!NQv%@wG5zfh_S8|LNzRJcZtCj1+ zN6Lq{bq?>&$_Mzyt7_%M;Lx#C!?+oZ43Ctn_2H4)AOGdwhauv^ft?Cq9ND?bpoH%M z7t?`i#b|jIH3c7Ij&I58;8R7uqoi19Z{rJS`ch39P%Apef4!}3*_s<(_x|5nb>;Q4 z{*~|w8l&dz;s-l%0}Oqm4rX_HE%^Uu?>_)-&#L-x{M=J+f4$9{-V4kOjKC-$MWiE$ zfHY%bRIG`_L=$5p{u51%F=}E=Vgspy1*C%@WngBQff-}zk8kUx#x&SV64z01}-i#Es44i`%Z+Myy{F~HyfW_ zz3|&tzU!Ckt=4Duc7El~#p{k#oyA(MiQDd2U8P;w$i`vNBl+d0o_p@=U;lc4{pk%i-09^^CE;Zu8V7-!l8u7qzzTWT7gsn;kuR^z)zp{AWJ%nF9w744-w>I-V{d zX{Ql%rNMu1fv&`n%Z?%rUZ6Q~hUN_T{tW8VpZ@fX8#h8SGD=uDyzjvqKehkfuP)6U z?en1`|7(#yq0#40XkdD+*x6Rs>EOattnzAK#sVAF!QgE;?X1h5_3|s8_nPMT;J562 zvjSfC;*kT#_TGQw{+s6>zOA!xc-`z&quE$LJ+*b~2J~Qesl9ji!Ckxd>^*oKE+g0E zNaTSX7~zzxvInO@1}2CXYq|zRGQt>L`s|lq{ts!4YAd~3`&eype|O*Qo&9&sO!RBh zleK=o*6YGuJUqWtuZ{WX;;uc%-|-jMa)dNWcI6m*;qe|sO5P$FxFz{N&#}i@J~lr^ zEJuoHHfk+p!(GIpuIpzeVzkn##)a#yR@=1W%*((1rB8YB>&7Qe^rZBg37t;+(f#}E z-X(achuR-J|zQmBpn^9Sv-6tPMIk4%b_gujf$;Xx4!trFSf6^XU`s- z7_zSjq$Rc{aC9(nQps;JV1Dxp2_vz>S&AelnC2>{Yg#^@r4Avl7la)Q&XdyQVG{kT z)k=8*vZymuIaKS)^Bgb-0V$R$nSmgY0E;L>`qufBi zr5vD)v4d(UgOXZ4ka2cdAb5$%dR>wGza(m=#34b!a`TgEDiD$eb;vXU&XD|}G5Jmc z^SR0K9N1e~#Sta@it^BW3=Cnzjb<4nTw9ti28Ias%3GPXVso%^3z%sDt?` zgQaA;&It^HC&e1NN9s zR~AT~r7-IK-fFMM2U+k^5W1a}g=3uu?_9d&+W!1;ei*U}I6s>=Z+^xzp7HEwKl{ux z&xGSN&~HeV-^K<&oY=qmhJ~$lbNnnZwlnRot^pR?`UOaFoRA-(p} z!l6CKcHOdY_h;sB|8RTvm$q-HpMKhAbj9T4_|%5U*3>v$$1OWHU3vB6p8RcBUUA9U z)05*E$)Wxe+==C2q-2>_aEHKkrg7pe%Hw5zM62gL0X%H{WpG``-Q5bZjjHJUUwLG4z(^G;aR%13B4? zo{nBs&r(LYmdWytn-i;B&%siF@Pl)`&mQjVZ^LK9lBt9q-{tmF8Wl8jIn-+_XHAa% z`wJ#teg5=1+^0H=wd>wfyWuZu$M^8ZHxOVK4bZd1-U_`$f7G^oNM%ry9Z*D5MuZwg zd(`Bvo{x=ce*h+|W;Rc1yq(N1RiYYE?oSigS#OJDlZ=Rg1Xpy%f1KJt-|eE##F zca~9pr`hXy0Jy&f^$!V{-sp{)>1j?g0F#QoA~ST#E{}O>9%eA1q5x)j!SSY8Llz(r)5a1Qec$rOejhzSf{n- z*r6=uJVi;l^k={TaL_s;)KG>h57 zqDBf?I29Ry*br0M2rA};=>dpU(#Ik$5Iqze5+PjyihA_u>3NJIrvjFhKD^2TY&{C` zIN~cnrou=~6~xiOsvkRC@Wh2}a~2skiULU{^o{7J3Y@K0sspXVqpqLz<2(KRZjHWT z+a6=@$o|FKu3x_Erd9slKzUmsu#LkJdd_p6gWJK*HGVk_0Mkz6^z`)X>@4C*oY}|6 z`9^;78^4hOBVj5GhS|9Zb`+5tjVu972%|&LE$AF{(eV8aBi3DB+JE0Id+xkx@#w*R zx6|);dvMYF-F~m%*Dq%%j*lR4%JC@Up$;WLjQh~YYiOAlm(|s?uXyT)?Po^zHzjbG z?{&J1$Cr-mJ^a9z=WhR4|DF%j4&1b5rhn-rXPtNc>F1ua^P=<5z?QhQ++J!gFZcO7 zCH+o&=b2ld_>{+AaruQiHm{%HM;N2`Mj-lgNJ7a0-jGm0qrs>=nif6lif>!L<8SpZ zi{J3TR@19>7i#UpwPW|y9{x=2p1-Q?xuLdv1p7}ohn%(Ar)vDt87wF45zTR&>3vS; zhL7MLlPGsHy}0!9g0vdLdPOFr6bkUqgkF{|#7iTKob#`I>gJvNm4t6rU`}>A?W0Ey z!BM>7+RwiA4}SN*{ik1e@4Meo-NMileAbZ;hyZ#BH;4$jxdeQpmOLc07~Qkg_-0Yu zfgGdvH*8hD&V7iR_ zR_n+g=lEs?)*&{;&6_rA%B|LxF{w1IaxgF_IZO4l$eUMeqYil@Dajnkc%qu0;j*6R z9qxBJbQil_ezy%An0{A3p245c@MPh$4bD4_bea~LAet+|C=imKL!7CgYnx6# zZ*m=OJ>OK|YJyYUUG6T;FCRZNx96UtxBd0m$?4_f9lBlQXUHiAJM2)?4Xe zH=LMkow0N4`iaSQyVLFVmwUa1PP^M_kIyup{ERCuy7Hon&)YdU&JSngUL5LOA$qtf zAzGtSMp>c&#W=bZr;%ec`^`5-n9E&Ec#Khh>DcP=hiZGTuif{Twfo*zJMfiSXF>g= z%?aJkUMJ-ggEP2E^WAo)#1oM~mf1yDqX0O*d#fDsid@$NWbVi*&=CFKz?c;wZo_narKKJQOp8&FjOY<&D|^PiqG^{R8GcTU&G+6%SMzn#Cu&)>}G$`XC_ zl!OB;v%#7>HXbn=P?-?9#>539^M2%yYUf;qocs45de*f|k9LL*J10P;Ev23`U~L6=PcZBCibq%MfPg=&kXL&(pdHuwPzc%j(;_yDtCi)WZp&ZfWsDNKV2hweD;|x!j_~Ru=O@LRDlX z^q&E~Gb7EESQ}8V!e>3~-rz`ci9RuCgC0z#wBlSV_~1AOz#m>-BLNw&#Q~(UYAk6J zd>>zpuYUgIMNh0xj&KOk9^3^E z9z1x{O*dV4-F1&V@<^}Oi@cM8opA~s_w@pMM;&)6fHQgKnP)!rsZafuZ~2yiuc0ra z+g^I~zFT+Q^x5P4AMSRRdkmeGp8ji*--g;>!4Zkid(knNXmAxVAM%WGXB7-0r}Cwg zYx2kb;GS&bNk3s!Ef;+QlZGU;bb9CS0fd`iM z-+z4HJ^l8vEt}V`TQ}W;%g5hfgZmB7oBuT1YBn3%E}NL=D`RM5yWi6{J*h*+#-_$6 zTFus@`;Odq*RDJ7d1zr7PP4s#pv3P85dTNsYM>svD+hliPe(BiKGSavLYlYp%QdBcHlOYEvI-_X1;#Z~q$n z$OGSLXC?>0|G4N?NSRXqGr{*;`5zom#QQ0~_bdO3Mj@GjoBz+g73+vX!8#e{=JZrR zShsFnP*w2lpsyJt%`9-D&DD8`=Z+oS{m=vV-gWyOw|(XITfV$^&mQ0AkOc{AsJe@< z9{9jCdPcQUTo-q>#({i;=Zh3);9i8zW$+Ii_iX>Lw79jXf+y8nX?9H z!<=%~+IZ1>-Wb$30H&v>U;gr!+gHT41xNAy?|=WuW5G*b`qCfzp&tT)+t2U(&hHHT z5dZr1>wo&EfBLGct^)nhkAC$3{Gb10{x>nO%Iq$lv2(}o{g2<_ySyv?+E+hX`!jtj ztY-q|o0kD2O2%Xl~!LF=9HOH#36VAOG38bQ#y#uB#{!4sa#f4sw=W`tQs>0tRmulB1utU z7T1Qr%{6kg&x&CkeBt=#e^xA<4ZR>W(800&SepQ%$Y14MwMj40PY(HIC45U9m8b|5 zcKWqW-__B#p>pkxpr{8gw~X>+fe}*7Hm$cIlJO(qeD;&#L1d0}3anvmeeQBYDa4$8 zHkwdf&J8u1r#xEQ8s|VjC88iMSp$%j;Dy*n4S9@KSpmxB!m%cFd?%}<(=v#XENdBn z&7i4H`jQ&LvMDyem#Lyhm=;h&DCXrJa6z`uWl2#`e6&_PrpW>Y4$eRhau;h&xQlQa z{ZX6xbR9Ot{32q|*zt1LeG>y6h)?cc3=GV2lW2^0MuM5|(rLw(Fw?DQzH9fI9w%XI@#nFxEQ?Ixt zoWpt@H2N2w38r|L4<^vb?%3M&h(9DzUlFgKYPcf z_4V-y%zzq>j$ND{u~V>e$$)Y#4ph)ffm9g|M-nZKC!Qzo-VpN&>-l%Ua_wE+Owy==bY(tXBzdn z1GQT|TD$4JwYmNLCZ)KOD@F;&hSdNd*tPCk)?5f@&>|eH#N^u)5`M<_*Iw{1U_bWy ze|GJ`_w6~+^Une(NU5qMY9AoBUjHe0&_|0Q-t-EtspXzWR-C zeB;c_44ldT@jw2@XFvPd$Pg~MzeUY;YD%x#c3R zs}KfAIaA`4%vy0(omvpONQAKzP!INBZpRAbsg#A?!G`n{F*)W#OELytVoUyElQ%Va zjJgd1EGM^6pTVcQ*yE>Oc~IlYNS|q=DhcNpoMZ#DVIap}xj<=f!douG?@N?q5(Zf= zx;nJ|6_6f4qzI6*#2reQiK}18qJTa}>0I%3i7FWlS1CQJ0=HD&UU`x*Gb99*;{eeD zC235_8i5HhbU;mYl@!@3yycc#9(w2@%l!QXIuJL+>FMdMTep7uw}1OpS6#K%H=qLWUAoIlhaSG? zzAyas+`-+w$$q$XGvFRWg}JsSO*uZAX2P$jI^egel_ z4R_XQ{LSC|%|}1_(a(MEbMWnhRl)@qT!5SYYhLr3NDF!9{GT7<_n$u|@C_{Z3H&We zKF%BS(;xkp8u|@KcC;S;9hPKkoIeSQd5X)rbyHZS&<$SZ%5`J~=VbgEg3hT9{x%3HaDk zZ@T&JM|bbPXV(GpT}pZjMS>$RGTM~_dhn`QB;yV&XWmzFx~)=mG-XYYW! zDD|pDR?#2gro}6mWY7b|RwieOg7gM$BO*A0;t6n5>a}tGKtJXQ-@(vx#X9e%kR5Cp zQfSrBz4MD!>{dE!E#UO~wzs{_^mCr`oL~F3Un{$dv{0(v_yT?pNENAeUrp<7s~c`K zjjpi#Ek&HE&GFVmvjM}%uh(%W`OE|R4!7HR3bWs6Ja!lUdo+M0ux?^?YMk9jK5j!k zsEANGqOpDZc3h-->?a@JWX)V&cC2NOue1>siDN(u_AbX6FqmJtVDpnO&O*xzwSy1U z4n0!acUSF^+iDMgrS`}zwMV|nup9ALY6o}K&VFKTVtUtn?}NLS9$d%|tYE9nE$~u4OZaS9Eq&@>2a6yTSUXTTLKWp2Lo5#3>x#0kc<|ss z`-+h_3h;|AzW8FyE-XFB-+c4Uks&<&=}-4B`yV@Y?63d&ulMZPWBxZQw5#y&giYur zFMR%`k9#~%TAiiZb?>Sj*{!+ipoONz%D}0ixlx%Bn#KT_o|zFds;ENDf~hd|q?v}S znAnmuC&P=vtI2_|yd=X{)GpzQisB(1sHR51W)#Jfkpl}8pp1n}YANF&P)*5_vVx)t zXzJ>q13|N>L;{&`K8q*9U?-&5$pJvn0GPiVyjQ;Rm6u(18T>}HF|voyS)AX0?^o~n+y@qq9_+U1E6QDL zukhPY`Jak>w1`gjzbxvIUCa_rEDwcHO_c^LP|AwelVABGh+#9+N&fA^m%sdFfAA6` z0{`%=v(B2DnmTgi2snrzc;JCen>K;35*2*@oxjuTv6RDUb93E( zx4x=>lS3tsczI)Hmo1txpU*z4byy_$Ztv- zg9dpdd?t&Q)v+yNit97^oUmgx`ri853L8A1o6da@0mdjYfC5)0!A( zFO0XyUxlU~A4>Aglm6ty_!qzO;O>1#DGwR#X7&-~ub|7=X@27m6t1|CrT&a;_pK=b zw@!Z&6@)&+N=P1JcvXNs9(A+5w; z{NC?%+wH-?H$O5RL{9-(jK#;A!f=29uo{}^`Zf|Ah+eOIbZ-8JhxfIY+sn)Ca!d*6 zFa5qzvr)&Q!8U`yqXL~Owl8jftE*La@$y#Xw!>J-%ya;#(nmTa0jwlDih8>{n5;xo zq1US|9j_gFsCMxF+JXCOhaRXMe4uuC7Xz3$G53~hPkd=@eEO@0I)A-yY3~x;MVE|# zaXNRp@lVg0e&xB-=gl_i$M)3j_*CtSe^ER92wTRFSfRdTq)3^edpNhj^jqf+Hq70S z;i@RCvX)Fu;eqS&=hUXxV`fdQcHj3%d%nkG-WtJL9HW**I8>5`m|;v3Q@H4kkBj;B zMA=t7aNxju-}~O-@3tAh!omU$1eafaIcV%+a0?%L=pkoaam5uceBlc{@!&>&>|-Cp zEO*v7F|Z(kb?avT+4sF}yp>8tw90;qFc$85dBb$s}G-Am}Jb-OiU$(^qAd-QJR(^`s`_$&64xC&$u|Vv`wFB}l;@lWb|kWrWQlhG9NJ43n4uh1npg zXRcy05cA_Q0Z69mQpFN;6F5P5Fp#=v6{jXnOe^z8>oe<`8@KTTt0#mj%D_(nVFfLJ9j?$$xnX8D_-&Rr$7Dt^Unvz2hETe!u;X= z4}bOAN4|RP(($9+&JquKy>9XqacJx5moxZe&C`xkj`sm|y?nVmT0n^eid2;^J-*gz zPkF@~JPL^yxclzAckkW}PXldv^{Zd~-QWG)&wu{&ufFV{FzpY+Ap5!_4^D*!;rca;JwpYbwS#Q3a<= z4nR4KMu-IK$=~xM>e|W}oqqc1a0uabyyPV>dEM(?_wtv&9K^Gq{cKFXEnBwSe*5jc zUJtpLfzNoxGu#BgdO&>n{eP(IFk}mwO^ULTDnt{9fL4uIh-5xUJ8J|>NKq9Ue{PR( z7q@NQI6XBn)@bqwL3c4EknF)-oSLMLR+E#a2CtLejK;P^%128yn7r*m6zcUXIU_Br{@JD}SP6>sPjFMuWOJrCuM4@ioulXDvE$^=4 zn8pidRou=%cqBYT6?gFouc*`Ic|_(wDyEQcFuqXP$W`CfMxk?Ad3ZjgtYE4K}#vKKHqgd)(t7FgG^` zxAV?B@3j0kF|fX{;UT{A^2fjQyT7~M9OpkS-gkZN)=!Yo1eBa_t=dDmG_(g~Sj>{X zaOw)4J|UpZgL+a7gn(u>OG$>xBSSHeN&%Udi(pZB5pin~Q*0@*o>8zuH-|44AhelD zvw&+M=@so|Xql2EsqpbTn)DrH%cNE{M5NH{^I9Vr=QxYdKk$OL#a)g6k*^=bx4TFil9F5l z>rK95zt11-=*9txS*(d=;~zA#y~tz{F!fs)~{cW;}mWO-^kyRjoTlr-Mo1-?qXM7c_nUk@DiW&q$izq z)>&|UCnhG!r@%7Mo&8Sx$e#Nj{>ruc@4IE;$fMo%GJlTWpFZXHpu$mv$H-6a@iVA8 zrg0Q_A3)E?C}bpZEq($OXf1mt*J@Y4vT#Mn6>wjknwo+i_)q@HKY7{9UWVbo{T&?m zA-GNN+qci(I{^77KJkg=Z&mR`y6(^ZFEp_V#_@IulW94i8U_h~LIgrZJ!=`pCt9=9 z6C3#9(gxZ;J3BcuGc_|aIWs#A?_y>ez9IaGiP?42teN&r)26MhP-{g0<4r`H<~`*lLZA%4#2#?l^7BM6mHd~qBL4V zeOam0j=YY9G8qj@G5wTRj2I(KJOF2>*S_|(n0}aVn2nfn7>-LWxdg8Mz4zV=vB}9v zOw00@sjw}N#a+xyBbTB~mXLgg-+hV%t|1cz#0_9hZ``(F%hvT6Pd#0hpIVKDwY?S{eb=ty@_92^5HJlN6?e35%166oIOE zGv#})=^l^}Dka94vw3xXvGd&T`w0t_$7lcx3k&cSKl#Z|;sE@D7rfxhU;Z+fBcCoh zIl^7ESdz_!*flmSWim@t14n~q1O1HLrcMa0@y5e*i+hhPj<&J{tdjA@SZo()XgFt@ z?kQB=#VgVW?hD<#b?bDfV17A9mLDow#$dN(Qs~rn5 z_;h7;#x28rOr8nFCJCT@%(R!NAv}2r=NcM#*G_sAh`6>ORu`>s>eV6 z@msfUMJ`ST@D+iz*4sA?&_DoP(QGt+=%4?ybIv`F@5l8!wQJtWC+%)NV+TxH&)#AV z5NUcc=oN4wpv-4kO~?UoZi`^4ApvJ0=FJdGK(UttfJtxf#0#xZl_o!4g~HNakt?D+ zUPx$H$(l1egmwQYf^tJZil{(hq!N=vFe)%7^Q|zWkk8d&GqIaiKs7j0($ac)OBF;= zj_s8oW$CTynI1OrV8ts&$xzYslv35Zr*Q=3zR&-{Pj@xCj>rmk5oQ!h;!5z$$t{px zA_Bkj2I-Ovhf&&G91#c>u_@Bbj#p3QQW?rpCj`TAy21s?+ZQ5dxtwn5XbvEG1!gHh zNp(fhlWW1nsKW^bt1>CXAwVTc5Qjavn?Y;B?u z1yf*=Qak`ig2_QtFjCTuq3)nbIJcq*L!DYmA;h3pvA8r@(H!u&4WtttB^r&1^DbNW z)E7;h_jqytwt$xfj|B%`+!2RWhhzHX zmtXGheZj%~vdb>pxpOBD@%%vK+Wi2Zu)TQv@Wc1sf8!?)KYY*9vBUlVDg%FrzuSkS zSoIb49yBlNaCN#yNK}S%1LZ6&Uve0n5K!P-U-_e)4w&R}M(r}vlEjO8`jTk z+%U6lc6##`bo|WB^wjtyG8@fS6BpII(yBLbw1=PQ0Cxh5*EcKcq*85wtjf1hWO=XT zNO>iQUjjP3+v}(A-yu}nYP-{&m>l1E=JpNK(=+4EBgf|9MnW}9t48)sTytcU$YI2S zb0T9byNd&3gvoaH*=LtI2K@Xx?zrPCU-=5?&6_uW$9H@OCM_*5jnNwJVz7|qx95~D z$pwd?3_FjEsw$Q!GXF?_+ttqoKxPB1AUI9ouYn*5$z-XI#Vhu4?^ zpjFDHVIWLoug=Z4U-13^LL!4>#B2fdd*A!scfIRfy0I;&abE7XySr z3Axf3oua#lh|)^J++fK{w55*5fLd8qbNCUY4Vz|edidbn@`*pV0fVWkrB4TY<53I) zNgJT6Uv=}ksbGf)E%G#>HZYN;t&B0?+=?5fEZ0_s%Ni8m4Jneq%;wq?UtD|1&n)y- ze(H1kKeTu82;D_BVYRkpy#9uBX8+|?TQ*NL>+=U|*S)j$m5O-!k8@FCdEt zF_KA^Gi=&0T;|1p_3w54dg2#<9GzouBU#In6O2CaLV8 zoqHq^MZz(x6dCaYvQ!LOX*8Lv`=o;{wWPTbyo9XpZ$+ZCm=INRAY3HIB1t%Z%4iAU znkXb0--gOLdSa1r(=$^LzgV-r03Qlf&M(u%)h;jF7HM96y&<@pD0e-{lDmlmuzfi_G_>De}%pi}jAxM}-XCNbabpYdzN zYx^M=Kz-N&0d_l*vGZI+&AkPC0Cfpsr;0x@67GMq(ZW)2mhN~)j> z#&9OzM>&EAtgmOgpXbhb)EBB_+ory*JiJ?dbM_Jo!2Kb@S&AYYPBH?0xa-{f}E>*!?*q_c;WR zY$d8mZ}({S-J`FD4S}rhu)c1UdZ)8N*ffx9N#qyc$c!e4_dN}0PU&iP?fuWx?Ut=B zOh;Dj(mgj_+z7R<#VvvK)iWZNp?%ux`p8C#m4l6!b-b^PtMlfBqKwo2AV`B{lM)A! zrO!wY1z2S+voFnx}@_Gma21$272H?Hs)qfF1u7nJwhIkT})&wm&`pGGQQII>3Y8U z=~~I`JO1wXBKSI^C-iaev10|FFoA3cHP~xriC)(I{MLhdzX(g~c`Omg0pYz3Km{_DcrdDOE5J&hgvSr`E}UmS_y`%2>Td}`&3A{;zzv0D9< zxXNT-?ZE#H**!WE5r!}B-~j`iOMrY~xX10)Cu9%fP|_AvVnv)3+!o)Q3VeE~8P9N( zWU73=&v~|A@Vg??m;?yLf|au+*Wi%Ee>0stA%xn|0kT-gCPkli;41PukssSQ%?(kB zn<9-x2=z=G2aL6nwn6kgvKLWeoI$Qx3#jg5fd3;(lt^?dwFQWC%77ES z#B_>?XLiUTvd6Hjb>X@Jp@%G#1g;>U1DONdpe{L>u7b@Xygw?cu~?vl(;zF6wsu?% z&MF1J=|%GyIFUtEn{1kv_?I>v0}&d!LQ)M%U9hs^gFJDnU>=QGbd;QLa8*IBuo-_M zPI(+yl|0_MioAelP5L4Bg#Co{A~pJ*_~Ab^O3a2SM^vmLi$bB1cya^RaSRyBs8RmZ zT`v^R!JIhf%%{5w_*t*3P%}R{r%74NS1vHPe%|$~W5Ui4&%jYp7fdL;8-K@}95k9+ zD9n@R9j}XAE;~(*rrK`zf4U@wJPOi$>HT(9VMT2NVo`7HhU040aI&}3RI=+hq6*}2 zl7l+lA4aYszAtWmFZeg^km3KWQN52+y@&M7kaM`a$KN9~g8|;v`FHl14)?<5ORF}S zJRhHn^qsd;fHP0gMW>D{yeXb1fzbSLO-skb&qQV1-&fTVHaA8iRoNw^wQ(J+uSmu5 z5}J1yFce8aT*JR;($Ai-GeN~~K#e;mH}Nf*7iDGzM>SeEe(tNeTsJ#XKfIChkZHJ5 zP==}sCX~Y$(OBT zKU6YK5f`{9xN@_Op%b^{@DipG)axnd_jyP00H45E+-c>cAo^>IGdgQ4)Z=>LVPzK-p_ zkMn-s@<=c0+oBq@R{gQ6Rr~`$uL}vHjTQ)5ZBS&6v~vRzW{c+8BX<+AnEXbk%P zVrK=%CNv zkl3&8jR>7qqx6tuaGK+5FsE=g8LU!KE*tvvwA8FNtiOJ)W;17D8l_Ia(l9Yg&KFwU4A3> z%uT=o!ab_g$gwm+4B@>o@U9s{%wJF|9v#CW?;pbv?nH|oq$NuxTf^-#MZmr;Q>5ho zbCD!dnH0AaA5XR!E*KmcX3j5dLM#AoVg*&DOoS~BJ%1m`(!4v{jl)_(atH@pvTotS zn>f6ebnfn2wxVbQzEHW$sb{2J!8a(KvFG`llh(S3)-)-|euOeu8tQa^UjtNpVakvx z0z&b^T&f{5#udVF+p{o)<{bZ(^Di@fI>$xf2*fvg{|=`7CFRbr8XfkOOK#lNd^fxu_KQe2u#q<<-FIphX4MN~3^_@e$%YFhFUuct-!B z>tmO%tNOc-rCK}4&h2>n+7tG?^)c2n`0y|tas2woVK`Qx_s9Erf})|jEhixI%L-l0 zxJnC_HmqRq?cWjWEGUI>m|B$6dh1t;YnEO^;MuI1%v1fgrC-J3w%_BNWUbS95`Fi% z7M%~EidI^-#XO33L6F8d2>ef&z6lD_I0*T+M8L6mWi{>Z-#matkMQSp^gsK^hFUOy zQhkZm&-vFQ$J9+bT)4w)jZ2nV7Rt7ttR$D=3dx#4`1Df@yO&wZl9%v~5;jMWgo(R7 zqh6EC+nB(i9?K^TQm9oNi2&&GbAonXTS7sVKXBN25S`JPr5F^6IXjadCg! z8vGG}3vGUdaYhME=RB-{ek^(!+_X4Q6{(4=4dtmUe{m9w~fV6Me z9Xj~;@I^1fH@m4$+Gf7YK$SX-^h*?cg&$Z)d#+y>1IBZYA4qBzxXg0{$T#!(IEZ1I zM8YNR7UfZHj{HWOC@C37?WcX;p+@pdxto6nP@DHiCtMJ1#op| zD>P~=2(EaGp7-$?#1~kR30=1Z&OWTyqfX9XToQTXp*Mu7M=Mcbh zqkpFci}h^{?A9PWo7XWa5;R$bIqLc=s%cFXLYMH8qPs>+TF9_YA!f!zBTCEaydNI7 zFw4H%l>Yz)e4o+L)8TR2o3Cym{<~#=@!do9vi;fr4A`1{aOyrX@p<`jjN=>KIWiid z;|r233;xLzi9_Cm9u35=c^V52?F#}a{S=z8=;FRf7pQ<4LT$}tC38pVYmxK4PjVHR z0YJPHdY68h16#^{)Zd;`{`hHwUvW2m+zz_&IR6NFJ>zlztNI%)*F@oPY;zyR_&Un& zKeBeefK~z_eKkcH27GFp|6;5(b_*ha?;R}d4+&1wjqU@VKtNam{&@OX|9n@ZHS^E) z4VAv*@{^fu>b}*Oh0ZqKsK8h>B`ba$7uofdn=KoSQ85Tv&ise8@_PhE`HA=Q0!b{Ef zQUvtF8PATCz-?TVSLlu=0Vb6s4V_9y3Q-0VZfx>xRJSrQjCLSfcH?)U4*TCSy-OGP z+G6*?_8RF6^`=flYQ2oqr(0zkt$;l(>f=}$@!RO89>ALuU{$HykIOAjlOm=b0xw|U z23jFjMec~_<=T3t1mivR4j1m9dLcz_)5RuAFur~GgM4j>_PaFK-?&jBE4}jXCR{|f zQw~CQ>vGBi#RWZ|)AKT>b(^tHx5sJ!2Ilr~tsqZF>xzZilH&oxeVN?R>e0AjgA|aS z7#5$xCB-O~Ap%74oVs5KBFLv)XMTJLFndpJ%M0DM@aTwZP1gH#nRTz>f%}$NG*qce zCS^LzHOoU0;&0ROL~qsCxK}Sg%PRi~kS_%K9Y9<98u7bB#ci=ZXc|Jg>CRFbW6l7* zib#XlU{-rz37IN%9hX|WWa2JLuL7T3uG|lAzjg$^D%G6{Cq`hd%V+3L7=kIRO~K%5 z1))q3q+vjAqRX%=E=9xZOGVTtGb4HhQGrt-&j`hCr^BfxbLofr8z!{q%=>0)9&Y2h-D7C?l%cSbwt6aYq7+FrcoGcB!9|ctSpod z=SYc>l_p+5uPgFVpy=}~kMP8*DsEy-s~}?qL5X!M)tT{3QmhY2m*@T~<(3?g2;L_z zuw^aCU|a=c=bvLmx_9XalxGRcSS6Cok;{4xQ%C|AOu?RL!YYB34If-Ug$PFeN;kYj zuExrawiELfw=27I^VFGh)IRue(|uGk>wB9T*L_h?tIgi(=jLWX)ipGuSPQGi*s}+5 zPdjk4e|Q7Fp!VoJ;2Q{i3^-2;!I52&TZ$(i?Ga0;V(+6XK#0q2j0XRQ8v~&CA!T}Z z!Rg!H&#PqkT-P#5irK(8Brcwghm6Y@pHH_*Uv^q9z9HTC8gAx|9$>v|PCpRsgtx2L zXhOYA3g`E)HOke1AUL4@**GmUY9>Nd&VRT%94r!mFmgO4QHFk8}ub6co zKu^o<^m%v}?GBK}DlW@ke$Eg)6G)Z6icA7I-x~zIMX~F=Y8f6L229<{wO(5nyBTnP zO7k1y9|1KR#MI@*i)aM_8Y_COP9fM_IKl<^V_R#8A`&=NX0Ny zu+LvBq)2%(j1L!3#=`GK&m9mNn$C{1T(Cil1}Z>yjN3QuKD<;!_xd3ThY~$r_rm13 zuA0&Sx-O|7=tFRPg32VbXnj(`-JYszT1|6A#7G;jO!tfW=iz}Se}G`)O%|NIunuQB z>zvph+CThaP>xte(7?Q^h9wF1lq%xBt|yBAD7i8$yx2rOrWVbXO3YQLjppa1PC2f% ze#gRJCT7#VHzBwLy#?EU-2JYOUsikG=Qr*^-*0pODt0S_R5gLGnYufd3vv=`06kIV z=lh6&iJSKbmkQY2qhL$@0`b=vn5)I-k^i{Nn{7 zdlr=$59iLL7Q_irT7JT9CW}G<)EIb3z3Yg&mwrFyy53}o)aJWrdG9H#U$G|HESfIi z&yPHGEL}a4`A?WW@@uA+5HWgWm1tVA{{I>YnhP*Ud*EqoZ7$$p0 zY3Ubv(~7{uuah_GhLXhtvW-JLNV1t|s~V0BE6@=tLzt-IQ8mI*@Czs@ z50c}GA5CFP!dw(_ov02#fa`~7`6V8$QzY;e!+Iw2$zEJ6DXA(_rR6Q&pMc0nluej_ zHL6e#hzrwgV&f4_W=*G`xvPi;sBHMI3YrUlK5#O#~PL`-n=fBd8-f83+wEOm7j*& z(;l+!i#!dV1H%vS(!@lLWz&hUk{CY=ea$1*MeHFHabss3yenh824o*c}`;v2@#AM`t791 zk6<7LGZ3kE1|AKEH-Jfr2KM{6qw;@Xkmzj?9OTH>uU!ylr~k46%p1=tJNLn`Gu!PJ zmLvr2R=e_oqFLx^*#sc3RwnQUW&bsNjZ5&Z4KaMZ{Cw~D9kBZz{0HDdB;YpBPzu;! zgei$$(l)5`Up$&Fp_K_1p2#oi>p5Z;QoM}S=2vX{odWz5Y**R=BT7+NSB0=rF;PA-pMo$Y zpK_B|L|`#jvzKl8*6a4hzQU%5P6x|5J?4=HNK-PY=S34P^v;kTOa$I2a3 z7ekYuI>Wi-pK+{N1z$fH?FPz3{lqDMtHpsK}0S4}q5(H5!< z^T@|uaNPqkE{qn4)db^bUoNv=F5V^G79Kew#TY1S2VEeh z))i_fA&l~rKE@wrU~KOsSf;slk4N{9D5=)G$8oIBs!M~=jwzH3A?p@>Q|CSMonXm* zGH5H=yqqiE)1zSurh2SkA!F#u@nS<@#Sim6S&cj?< zZTrLGP3KpVpTo@wU&qHYJpteC*#7+BB&-wuPZmk{5ZjLove}d^B>0S>G?bB3_9wdjB5 zI>j)KF-}hCx-PSS>A2jznO&`MaFv)0fLO=2!d?g}*RP^O)c~##cT)iJ%g?V*$h|V( z^RnE#XtUi{hmWh)pMJv6el5WM>e`84XjNz!hH9cpo&_bEQv0Eh!mi8!jE(Tw8==DK zzYm(RfUZ$kuqLn^9-_OP!fPEHy;E$-+3For_}^%Z~Zy>p&`J{+NPj z6+}VqJU~$dT;t1h>E=ic3$l&UxxByJ9#t}m+KnhI0Znh)k|tl!T;E03>`)90LzGa7)MYjYQlSEfvwVB5t;k;MX+ zs#p_McBLetlWaJZR4=nIziwuGd-Z7e(wuGtRlTF?)i1Y9=V5B&h!0bUNchM35x*7i z#oI@an5wa^ANc=}98W%f-)19rQI-1*v9mEi)`jE5j4UlJopH7Bd{L?SoIL2ZJv}lX z#59#0G;i`LeT>~K9V3pAk#&WW2)NS)6=|tSl6kt9Cgmk3V`BP4DP=AawCko6f=XZ> z@$4Cx-9llgVR6PxJ0t9ulaOO?QmeoeZ$QCf_0yYyQeSN3pgN$?s2LdOpkFBNeH1n* zm*>O?Sc8(l(I!T|<2YZp$RnnPa}39u@UFj^8})EZ6hqNBfnadpix}a(`ck(L)gz5B zFraHdVUg!=XcQ(JEEe6OQ!xZP^5tL+4iB0tvUL>( zMkGr}BwiBY9AG+6yeEXFAfvy{N}yOKFNsISo9akOjvHt5g<6uSAl#Y3U$XSD+7?7h z2FkB<#Fi5YLC~NRW-5b+`HDj-v{V@@4$&PjW4!m{eJW%vBqqn_OD;FZvOxfGRa$U+B0!)SUe0S+NIl~5nj|>y8&#@v2L(~nj;50E1oxboFC|Or> zOO*K@p1AUacvm&fo!v8zW){94*QW#XK~bX!%_p(07$fZ`#>*?9rEl8qfEu>!BSp5q(MCw6}h#0un<*Sm8D{Cw0b{n>mU+xxI6c(j#ebhp(FARO9@u@L3P~AKQl$zNDqG@r|1B z-%P|fV;Lp%yCQKZSCAG4KuV53MX+&rx*I;lPFJ8_8a5Wuwty|*=c)I5b(($0`}^(W zXDsd$$`Cz*(enjxIs_h=`#g=iZ(`}mx5kc-@g5KRKT_geYVoS}L{Ke#dos9^=}JH%cYo1FB@ZDdexXv| z9^+EbfvjRsGLflTQHrAF&`06JU0EU%yU%)Q#io|$JA`KTbN=9K!_M1a){1`1as2(i z>ENEZ-0d?am*%~kv?R7fj@5U*D;d?%p5FxW96#6 z`_;@6)7$DVs|m34!E`5L_schF-WJ!-n4nuv6`?}qG6u~LK5R$v-#+v2#hs5RfiKb? z1x8NBfP%r?MnJClV&$(~+x-N9s+NJN?TFnAiQe~lM!S67-t+vecVgG)PLPSnBnJ=I zGfMc7^SST`B>m`%q2IFsWmZdCYSMj#WVI?WSZJbIP|jsU^AfmLMpsN7q|I`0#WV|8 zlv+{O4Lf0CYKd~wh?hiN95i%@X;4)pCKj7Y79uM4T_u~#1;Nx*f-2cQS)7qI-E=_B zVXO8#N*sDptYYa1h$GQxt2#qse2xJg|AsKaLJy=-l9w!SB=W0IE|0p8GZ_9)z=B$E zQ8?B|>ZUf6zgqI6c@adhMOk=)Jz_zNLA455(6o_KAkwV8g4rU$aMK{gC?Rn_YN4`} zGI1!*h;qTeM(A?Gwt(Jz-MPH^ReWG^O1hE&Mk2S5h=aPY1KVivn}0AmjfN-&!_opt zd;zyKQpuwkqlrVQI!XVCl|rI@_Bk-+-~Hm8Sg8RKKhIx{LlFa!V*8=Ewy#ya?=*gD zl8x)1dxYP6s9k3>L7DX(p09&`B$FCG3hV`FmQxQ4%|ckNOBdY{5i?|dT$(Xb@UW;J z{je<1Ce%XSx}@-9wFgOjyy%xrojE1f3x;MEHLJD<&SSF)3lfatW4{jlh@anj!?gp* z#S-EW?;uN}6(WqfL1)yKGOXl8jeGi%IV^>t$mbK}BP#h_F}(!srC$Ggy`Niqw|e&f z3r_)Hn@@nS2_UrX&rAoT^7E9U1_8^Fz!L{-u4fd2XtFA^FngGT7Z@4wAvuy4(8Yqn1~$b~pVY zTVkn*vroKv|Ax6m?R+1yer^$ekcMV*xXjGVR7&|Lut16M3PU>n9vmaMIXE^nKRmj8Qd$~az5_DSUbDKB7b0zF zCIW{~Nqu4v5oJ{+$)fE7Zpd3l`aWm;%E>^Zz;I*kSd>KAxj5t*LxYkmA%recnfQ$r zM$Iu?R&uY`DP>;|?K3cJf$#@U#{piJtG!jOv#iY}?YHO=)%AiHq1f_Jq0^+qz@f&! zullX>lVgEw+x|r3d};$)6Xa&2~R#&A)nz*=9P?>NalnF`TU18!yH_ zeMY&wFuW$azwczv!VRQkqZ6k%-Mkf7EBp2QvYX&Xpr>?&K-WNx{(umS64z-{Yr$pM zJLc$t7DleGpGDl=*=V(7`;1s@%5wQwnzqd2)UqzT0K}WFGofa=AI9FQiac8&hF_2X z^nS^ZH0oMk6BtA$9uvxb*D?xdf|LWgxkRA7J@S}6v9gay4fB!bK#-71MPxOqeGJ5^ z8mIa~iQ>Y2B(;e^hX{)(*sg+5vXH0-XpzB@r;0@x$iKa1y46Hr;tXn&kO+#)aU^n^ zf6P=jN_h?0&Qubb>2ki!!qN;s9~{QR|DV4Dg1_=GF{@j3s``m zGn$N}sAxYOfVu#% zo#g=Fr<m5pDJB!BMz+6siT$kw)b68Zn=Ilu@x)I@Vcm) zVZI0Wu-Aq;h`6H;mX=GYdvr4mWC&9Bn1E>}c2Vcj*|PV3=akNKbdk^sj9d79DcWgR zj*Wpzwlra`NCa&U@v*2#-nzImz3xx%+`5mw!3}a-?bmF|KY&*9ZL!JoE06|ba?*z#US0<>WplOGV`t{twr0hRdH1sII@uF?pbNzOT}rDU zpId}OaVb7G7$K91SFl!IdA)B{s!>*H;t{$)g=&w|KjsozY={X~92ye<{b|R~3M-v# zXl+>Ca`bN_Nms?|zN*Bfx#z^y7xCOjZ_W?GaTs<*9mL8(X9CBZXJTD;;ZJ^nLJb{R zFlDhD4Sruh7ruyjXxE$Y8Idi2{&uy^pGMd9M$4Uho#v|(rv)xZ{EbCF+Q*LkGE?^S zA6EVH6~AEQptRek%aZGV$kuF6$L`&%yerxHh^G1=H@&^Ci;$MBK9#Po37WOIrpH*o zSN3CgQ#ADCgGupB8s*fWJvu!N=_hq5te`DPFi(bM9Zwsoj_iH@OteT{igp@P7`Z^_My@Ta1M^t8owsr~iJs?-*-9e`q+q55>Z z0r>jB5~X?i?|u5*HJwLL(bIoVQnh!=DKeFEHkc4sK~Zho%#Cdhq*}!gl_vMI$zZ^v zbH9hBydxQyQ|pJ4C&3`c{B9qOk?1hc)bu2T2?H!-UPu6x69xI(wU$pcr>b2e$J?&$PQ%+=t`Go03mA{_mcd}; z)jL=UCc~TV0UVFm$WHYPYOCG<2Dbk|Rc~GqJvSdcH>19nqf@g+&LD2c6ctpSF9mUqpwk1wy>w<0T%WbDex1ZM}g|O;6@@Ev8!z(EW4nBTNK49l6&Jnj&2g}8{ zhllS-MYKp5YIPkRZ)b}agWzXLzo*Pf09wRzQfoVL2L~6z*tYvg6$w+vIU$cH*J+0H z=XmE67s(@f`}Rt1@C!dErXnnz3pJ`i4Oq@{w267`8r<|0h=Yxhx87*9!?bGKvLD*! z^SjKBvt>=T!Ms!p+FrBdyDvl~Ajm%pla2OQ%zdh&3#J|)DgK18aP2rDm|RDNA|tUx z(grl^^ElhcpW#1il4b!Ad#;5ATy!-$UmY9h^rkwUb|tlK7o{5m$iDD!doaP$8*s{L zd*}WtE*`ldQ01|#)Z&52@bVMFhZ73XVUea2n%YQbDnLw2#km^2-LCygmY95hU5}5N z;mejY4=~=uJ_ki=mAE=;pKt#*ck-%r{)BGSgJ!Gc)*Y!QMG>cK3g}86qcy5Yi~`6L zWa`>XXs9|E{w+i-3XKeECC^xO4gBqN=5@ZpiWM6 zA{ZiL_Q)$K6BOXW_ST|@sSnWCR~2+!6BEGV%=U=Co3hu`hEzz0wXayF8vG4`gfnK= zOLGi`3}o-o;WI7uklh#&lD&sL9bw<#*?qIIgdH&lz0FHia6A$#M2SMxyvTq8>(8X2 z1yVU9W`@MY`LyJzq>KNoe|6Q8%4{YE0MYL=utnA71}aqUlg0b$N^S&3Jjox8xB!7( zmM_X?dMVD96i4ckU@^^RCzl+TFN6bA=X>Y>!+}DR7*{ugG2V+Ss%QgikZgxx5lvbV zc-jmVhf)smI2poecxH3}bHXg(XGei;M4C)(t58`_bYVG~HlKq~V{hXTAeP&wp zh)(Q0#c>SjDkQSg}`(eUtLjN%CA4F7d-hL#>YMr#Uu|EBes?#@nZJT=2s#Z)K;`U_9{eScL|pubc07P(8-I`3V=B94YpkN&dB0Ufa7 z-x{AGP{y3&pllJW_$BC9NuskZB%wmNM1G1I^j<+(2_@{5SX&M@h(o!WOxs5s1PKcl zsd=#9CKRuXs4f{$Y)PpCY?}Kqh*d{^;fw@YdulvPuAC%ZP$;bhT1}D2q$Ew$a#|5! zRRCrWcx~ys{;NJ+?c6reyPKe?^?sYwqthTcT=55|ZC}ucozvB{p|5R8vm`^pa5{bJ zd{{&cYq37Bc7Bs7hS_>nENfh|pl<;%T%c@xB`n-LDjK`ibajR4P61a5w+P{~Zn%23 zNLqSgHw%Y??lvG_yY}fHg~OLjy0YCHj_E1a+RbY>u5JRMH0Rb;$N;jRErmR#BR<-N zhj=p!J%y`iw!i_-#7=9@!>4?zB#?{(8T?ym&;5v;Eby>AGDh&4v>{hS2-%6qRv1qR^U~WhUFoYb7!ySk`kKQF8hnrr-Q$xwot(`ruG3K z3CG~7(Kw{l75_yR-p9mv9RV^2A-8+omMuL!eNfxm@&EV!TTsGRf3&4tMP0{n@;HlV zN1?FH>c_cUd6Y(fEcXn;9+Gs^B!1>OwBpyI^RE;Hig$UFl@G%t&wA9K{UQeQ6 z(_`~hcnq$Uzp9#&kB+9`RD`=)pbEZZ`HDLc6MEXk?83K%=~YH7TwI+!k?Ys{NgAPhuDy_S|3CS z3`o9zFcPP79-Z$FoCo(`%L@>ODd$$_YT6kDUCqe$DPi-}Vs$X1WgAd)n#L6ULR*Yn;o9!kKUn^$cM1k8pzVfGN)lSvVZ9d&J&DgwU`Q; z0{ke5_2NMB^W2BZw6S~jFxgFP=KPmOQ!+dfia>yW^RN=Sf`_)Cf0b^(L=b@d`NX8^ zn(ENNugMiXO$nVYJe4m@#SFtBS*l1eFK%pbJ#vn{suG%nbc8D$xYp7sewyjxNUj8u zMX?i(+uWd`wn7A66P{diXZXc9)}ZiEE=!24ORB+G_r&-wx8uyH+%r}EpqCy)Liwn- z9NY&{^51S9wF@X`pw*NEwnE*|?68Dd2s2q9_b55q#2WQ-Ij144l1b&NDJ+^9aT$J9 z_4;Wx$uTwOdl`{JpN~v&hu{t!l4XM0u2h=KM+W^1>um^yT1ihhZ z$a!rn_Iz~qegMiDSZ3P9_lJI9dv9Useea@jxZtwwfRN_Af8@APgRM`NAqSaKSN~@1 zbd~Jv+7%6cVu8id#tA?fa*cy&)si}E_sa#qs_Q5?gMxcRY&#@Utr)o|P6jF$wzj_#P{P8g&zp<_}G$%a?kfhAfB1xkJ&Zr0%2-QgPXyX_sfY?_b#XRgyv zPxoQ!OZvRp`L;p&t7>Iv2y(D|<^26&m3#OvxEw2u+cM<09`jtdZ*PdxoAX2TXpYOZ zN12^^Yt8c1EY=T#CDzEUrv9HhM}k#@zY%YLC17sqcMb&nK2ra_Y&qUMv2Qj1+=W(nV6YD(W9cG0F`=Mjb?F^wwox0Cr{C9}R*cx6dKw2^F$LwQbKSDd_;JE5`&`s7d6C-(87&p(P}$>JW|&W9rq* zvV3lvCYagv77wKxhsIB5hTSXIOQxkBuaJHixlp2bAl);DWtiM%*NWNZ6_uu z7!h+^f{7SN)p70jc`ZNpL&oeUfJA4qWDYoI$d)~f+_+VuJbju>{_?HJu__d*y{e^^*R}xks~_8)T(SjV8I049y~tAC(}s!`=82@b3C@L zb_g%_OX^g~Vq}+DmIZ2v-(7XCY^@~Qm>h;Bu7aNuuS&+I+NRf^cX?3nZ}SPK&ICxf z4tPqr_wj*$6{sVMJr%C#R|UF)L&F5-F-0bL0VQidbGYb~cDFFaB(nPt#wf9h8TLcL zM1N&R!T#>@T`4exP=Y2tOjVG_VQ$v;WIUJww}N|F&!n<=CLYnk7>ZZ;`uh* zp>P=Go^~rv0IqRG1{7Un`nY0Qf(9!P3_c|h2C0ies`E0E_#bLhX0{YbMhsAIq!6rq zW8yQscIuRI@C=2BBJeKY@;5q^d+d5TcURIe!^nDl6OF7)gf#`>%b*IGh%Fr`cm;0x zQs5Rr5kqhO({>VD9hbMripSvijsTlO}TV6_X<9kkRxYN6O=&)H8moIlLGAs526#I23 z^M)kmi8gSDV(46hZS8HF*=cdQm9ryAmR3S=TvF^K`2 z3`rk?j8b+f9s&@x33Xd-zdD{S1O_4SoE&6*q_6kwzEvX?eDz(wJmF9iJ)$q^ zB&}KKdOz>~D~tfxv38D(J))0KVt?M`_~wW zM#9e_H9i0yv>a~Zc0trE-#32FfU5yKS<$u_yqMyYuE85t1m_*y2$OanMO6)vP{Z@} zc0ZRiC*_-3^7HJ(R=f6B*hDlVHO3Q??}?l5ug<9d=d6A^>*IfIVs-<8&8psMJIoyu zDcT46Wjj0j&#tvYU*EjC7Y*-dTlYM4bP{+!rh@~sPqX+Jb4@oHH32cUa3Yz{*n8n@ zw%Uz?Yfye9rLnz|ei$D}XJnHzHL+WCmPj^@vq0hToK>ApTB}oYW2sU$-P%*G+kg+*XOY{q8Lk0v;p-*l_4#`TxqarD^R>y-YS zg;&Bd%TobC#3eZ*rSIFZnRtM#wN#(SI2C6aJAtRqi{8elK#OdTjvgUGj&Av>v-bC|D` z@Mk1`%~9$e4!{|=(QVM$aJpink`zI4iFr0qJC#r4r4%y45b&xNz*DF}20MZ?sjQ%; zycZ}jo}p3B7NV$j?`nn)m`e85hh)IoY|u=zYqJIcOi(b43M~v;wnU>v63p>cBOS{g z93V9$;NeB$fWn%nMpWxY7SQO#90t&_-|#&WdSHnY`pSvpHU}z_Bw&0x8p3}H(@`U( zpbW%`CkvFx2H)5B(BGW;5A5Hn6N( z>z~RvK<{&R4^tee zG!&Wqkuatt>hS%U%8}GG!_vIM;l~ugiH_sn8HK1(#7qB}1aK(x7OP4J-;-W83&>E- zK|LrZo!<%?i1!p8z_}JU@}Ed8s0oN=;5!sb1)`xcB?3Y))gfmu3 zLb4cECJ5FDkR&l!tojCz^(lqimgl>uZtPocG@Jd8q_^62%=(|9Vpk4O7MS?IFk7(q0Dm2PM1bvQ^^bZz-X@und6H5Jmsg*p%8cdwh* zAc(>eAl14f1lWsse8rOkcKF23UQ;t2v5xuipNNAp!TsW4*%DLbDBao2&xrEWWP$AC z%c9bA8KxvC%atHlGw1XK_Yv|Zw(eicV&dXzcl!J?C-4#Y4j&ig_)gFmSXrO`o$z9s zY4gU)dCWANSQzkqVe-@N*}$q=M;QV-aGqOQ0t(M%Oy^sp1Q?bnq?wStVA zTrO4skOH^O78`qo(vbCa4Tk#D+}ELBJpliJg1Vu;#X7_We(z za-FPy5pf!uA(X01-YJ5RX-ZM{GvbKbKI+AlEDQ}BY7M@gCZ@Ht^jC|uCgg<&tZ>@U zfWcmhtX=f%#ZU&8*%ZF1OGztJZyPrctHlRO`)SKy2{FMyisV2&r>*eKxhbvOpEh8x(ILhF>&&wOUJgl9zgghpZ1*4&y0<0q# z0Joap^#OqLISPfx1uTfa^2hqH@Yh;zGJAKi?z+WRbwj2F50T_ArZ(CfJS_`Dl0ajT zxP@78SN}D8wQGG5r_N=et*x0m08w=pvBF>tO)L)IWbDd7;LrAm$uj~PKGU^wgUl&I zqfWI@hXrU{6;L5gt^H-{Wv?q2q;(T-msvqH&=YI$!7f0QUX0G?;U+UR>?(29TEt@v zeG}`c|A<>cS=BcgI*GpY(W0-2nx-T8EpN>zq0xr&}l&wG(d3v6s$K*8&w>tNHV}dppN0tUMhzF4}(B_ zzfpzV%l?XrAh=R{O4gWCVFHL!(m=4yup%VLQsCx z5~U=kS{$255hbi&d%Q_B&RL;O7bi^y#T}D9;VfIFY{EBE2QdCdWkI zD8MI*dBvq#dcD=E3t%!&q()1A}b_{a-tYnp-(cJ5hW?lIW^lRd&m5C(5m~~ph7&wp1i*R!```bZ=RC*%uCuVP@TFICwOH{H!kHd&__#z?pc&O8A1d_MY~sc5ENS}EPP#( zsIRDsJW}Z@!zdbkoe^XxDBx122asxU*^10aHKTFqEB@tK7hd|ftFFdT)f_YpJ~Rdn z@ml9y&McjY;cDWT3&K;a-EQB0^9>*W^&bcAV&sDI;FUD3ZPC|)Cbqy{e|%+S%h}t` zx%A9!+c&MBndY~i&{TE@t5cW5P%{%URkmoz+=QYWP)StbT7ml5Lvu{=J8eFG7g2Dxl@*UMz#@^IrCoXIyaUWlwy{$T2#2@Zhmy$NVe{>jh5Z zjvYH7qahkSMt6PXi#Po~zl4+NYK^=BnO)&Cg;&{Kl)XVHP{NsP3@@b+z+ zf8&3D^xg;eLo@3kLOG~pg7&?fAq;a`4Sp>u97Qy+)o5Urc%BRzO_RxNcY6B{E!8>07_;?`j==^^0Hp*zlY6vrpHf z#a5%)YP4F-rQXW#eB?_ZRz5}d)IjZuRp}t^|0+~ z@1vs_Ut^H|=1SA1c~H8EKu2WK4dj zSe9Wam4%pVu}R29Oftn_ktsmT?lE9Jh>O})H4F=F3Cy(;2{?<&rNlI2)kuYotuuvy zWOQXM{CBij@<QuK8U&O(g)7=;zEzMcu0s5?A(g90;5YFxgl;e>U^}QPsP+*{DL+ORw}=W!#91!<<3gG!zYSRnI#x z8mz`m05CXADdCtVxg*j9AwEf~X{%_3>oFiBJ&F1{Z4M%pK(Q7vKq>qmiQ8g$f^c+#(vqU~f^fN~S zXEz9~@<@v9i`sIKcQ+W|7uNUWS*9zLM~x2@CRH>Lt8G0}=O z3#o4Z=-k4sckcO{Ywv&}x^}A4)%s-{xDO|~K!^uGwJ_-{L+^nqG#!=W1`HupC7*v(J-*VQs8J0VT^aE&_$9 zomC`v)WKyPbF&eXI>iJmmbH09vbKR1QCPXC5d>Js0-TkR6_%T0J^*3sL_?@nG>(-M zjoL($?xOtx2e?BR6gx$^VAc;Cuw)}7!?=j`Da{;EmSd{2u93z--X@tZCOA~9idd=| zz>#n7OD2Xd^A##6C#f_^D{@)eUk-)YZjppBb{;{5vg9DF zT-})CVIrobx+`>`V3Ntf%#0E#0mT5jA{neaMG}bVWNplGAte&T(tZ|E77jVfJIRn_ zf-Yx);XD$Y2+TK+`A%9HU#rQBkl`vtX(mYPWT+#i3Rr@XK5>e>p+0U3=dj7QHgU&d z+^WM}Oxvl(3k9HPrPk@LEVfq`msggSSK55Qh?t*B)zd{jY0SQ&fkLu^bT$EfOk;C@ zD=??6>12SAQ>KQkt{1gEP<4@ zwlYc@+Jtz72#3w@JzJku`65ci#90IC9|a|8Ldk}rY!*|{Gy_Tk1wu!d;H6m`(*%$` z+GtOVk_idbVKt=w>w%U2*eZOye!o#ac*dE>H*Jwm$an60-F~;zZ@0V42%T;Rp^eb% zbb4)uF2A1?iEg*o(Q5Hh;rsoifO`;E83VHvTRGrH&iLrQn~b3QA&dxsZMaFDqKBs-&c=3eDCgucO5=_IBUc_ z!x{6$XI$js3~GNKKv`&jo}-W4i}UA&n+iN}@saqsNxs@}AE_rM6<(mGXU! zrKWhGYX`g9hT$5ToJxRbmJV&TZnuw{FK)phxmvs9OE(_hyZe!c9{M{OBk0-x-~*WV zaY|I=lo9og0_w&QVtR&dHVj|~0kzr#2ae@-3`2cV2w z%sDmGhh4~U2`5#Mp)ZlDV3w4-Y6O5)NM3nit(oR$nypJg1~Ndv^?H-CjI#`BO(vDJ z0hB4}birf@PnPr*>J*G@8wddGFeY(4 z2eNroW9X?gbC)KEcvBpP+6imY1j97#NTj3=0 z0x|fS1hvUPnJ$(eCz^yNKZv%6Sa0kpGX!6rU^-T#;IaYlfH+=sm7u`Gs^1 z=_?jKVBrjMz4?a2Li}S{rfDm#x+8J@Qx~&KeMb2~3~|UP6;i#y6L(~RwSn9eGRdjc zJ$7jB;k)!eaXnAP#b++Vd zUv-D8pVaRXJkb&yfNQLe_24hA6uj^%y#fy0JW8>5ZlRN7bBQBmXLq`_g@@LnvQZgeqT`PmcM54<;FJpL3$J=nAxVQ5Q>V`pC1c_l{ zVB2Zy6Q)`A$fF02?R{wfuKWK^#^|BD?tJut`{G99gJObtnJ}%kV83U;p^+RLaQ2$;2x5y&=RE6Ie7K|nG;i{L>M zM;pkfEx8D55e=Po4iQ}3_GJ4}ma5Y8$)ZLZlgvdC#-a_-)&mhw2D)b{z@7{UxN6EP zxazEpm6q(!0Ewhca*P022^8ksFgIzmB+ZaOF6#i<+#6Y#o4S>hs$ebB%2+l-l|?|O z+DJLqEUN(t{n$W|#s2ZEu!w~Pl3b^#Z6OW6tPaPpPmtg?(z%E2pc2>_{5O|BM5 z`6xn8p(#s6ZwkIHKip%|AcA8E9^tMC$++Kl<%iG0V2k4anwV_kxdWE?wkRvr@*t}SRK zEyg(acJADW@+GcWl|%5!)L#fw#5p5j3waG9fyp&XRUxs$f>B5&qD{GVCdf>sc#Sh9 z#}@}Yj*mP>H;Dt8i!&5AA=iMUL{0)#?aB-J&`=I3S4pN%tXr+4C^W<5c$aMLGVTGJ zkx?*MaCG6!J@JQ82k9-eno!Zdu^S@6U`uJQP(-Q*+-1+(Wy?giW-o5+aBM%(B_49KN z-o9G>J(Ux~mXyN@n0S?$b4$yI=i2@Lx{dsBAes&Vbfx=-QwuMB zl`%qY>%bPyTr2023T7q>408x^QLsL?(pSA>tMpO%W?z4Hc6zee+`IQ+2X05Php>69 zYDi(fy@-sJ$@R;Buw=l@uuH+is<(V?Yqt2l2${{sXYYDsx!boI&*~9K?Jno$A_Y=5m_zQOtYD@cMcBv{rjrd~ zj0r=!DS3qvN+N=cgY__g&zHnh#OG8L^@OUr2yzerR;=Yghp!{ z2sBc@I*Oo;b`;V7%+-7e^j&> z>My2b%Y30J__d5+D&OE*H@4td(ZX03-8E`$mbp<-+z zKgY;J13!Hl&k{-ViIrC_J4#*Q9o6H--{9vL+q65Ydaj5-7f};%RSPJYCD=B^YH?U$ zQxMDGsD!uGYBwO6q2ehPN6kot;Ns?6ZCQhI^(P5SDnDII2n&n;OJH(Pcnw&0s^KJv z1EOn8YnRLbT4Wb7o+t*bMnz*N97?CTx3_SdZJ1ow?$>Z^1(ueUI-O3t-3BodkU4z#@P2{cwzBWx zL$_Uf^q!k~OY^SyYepD6D&!`VLbM_R6Pz^g%odmX-6QkM{r=Rz2Mf-)kF3q)fqnIOO2?ruMA+wnuk zj==Ni#UUP`+TxM{KdRSToa4J06Ek1)7|lJpd;Z=Vmv-H!S}Not;U9J>rdOZacX>GYPI!k zTRMiVzV}04{P?GCfo{sE3aI5U8d9$+bRJA%gXVaG|BUUurA2;py)oYI(f#`x#^~VQJ-cqcY4`27wHKCH8wVw- z#?{dN2(?(U+45~5P8HQZ>H`X6tgj`PF+o#Ev!<6G}sUd@Dj!h09hdbdy@Yac8XvviU*JxwAn$; z@h}0Q0WTTjMtS?Pw#+nb6fIR1p&=0iW<>ww+|>N64ag;2m-T1bxmHV6$!25$Zb(#` zE6SXNlrxY;%3GRVR!OkG`Br+=82Nf31}{q%#eL#nArw(Lm2{dhl_{+)nO(^L4dWw> z^u?rtN<=w28#(YzBzYkEhjyz-17SD`j}ew&jd%C~)J}JGsk^$|Sy}4fh#a=yRm?cC zT%EHbRboOh+UWqWp}YzOMX=$iEaCwa#nbR2Sa})Zlf`E8Lg8$T$qd$|eybpoRlRFM z6-0az@TiE|_+n$-UO)` z#9A5>(|wvS*l50;qW4hbE9#9q5To=(i)_qDXhyqrjzW@iRvNAL#NUiMfUo(RQ9t~1 zFZNn1f`$JQu04xT#Qu!ei%}r3MNTz%in^%qa0Hh6QPD ziAZ$3GD?~&d}5SVj1DQr;%Kn)&L>v2P-}M6`g0$@>Eb6%Z9l6uG3EcN;u0P?9Wgyt zRxsQ+3U_f5KJf67yT5e&fm^%tM7Cy0at!#e|n|2{le4EyztD^Puqes zbdtgX;YU2wF)SfZM8oK3eR?quJg%T9mcd8`RHF?f8fyZZkL-oMftuWtE={Uq?)rEXV1QD{rOLv*orZlJoPc^wU;}GcP~Ep zm8D1STv<9si`eu^Igq?fRa7Y<2JcRBw<=1(PWoAva0X2$ft{0dAzYL)(61|v`Z<@L zdC@sL$0sLi>(`UjZnHzxr?rWR)#GzxlhZXgM{pgdCu`l_op(I+Uw-=o(1dACQ*|S) zC|0FrVfTw#RbUXhc}?<1Cb-q0b*)ByRgJ86LV~r9jU76&wD<7RLxdBI& zKrsYqvwMaZv<;>(t0i2cO$Ew&vTy{+>x~7TBcXu@0i6X{qpWhJ5fUGOayy5J?IyJB)lr>H~cegkN*2&z(9LF=~LTLCo>hh<8?2=pVvM zq@&2V%dc__+iYBdYqZ76Vmf3Ldo);~I>|wq6+nRYr?R1T2v$WUCe)75D3Db9+)4@L zItRhJh)7m3y2ubDrdo4qEx0;W=~2`Dq|_BQTRlc!v6=iv<_MHw5W!^PxV`3`5UihZ zG+Jd!L7cS^l?AGGu`GQ>m-l%>4GD#1Swm(NZ681>ho;J0MtN0-!m-+x@BNp&su~=9 z#4X^4W?P4!E?#)yg`mrg$weTL`L}=gPxLHNA3Eh5_#hBr(Rq6Z)+uU{=xq6qHGnmEag^Z`S$t3Tj`!dE>fP z^WohGJB7O#TCz2Ut6u3YcjpdujveSN9$V>lumn0Dqk+-MVPSsx*umukyB2qSb>aS- z+WYQXS)R9g>MNQ|kfp6J(z!K~#r-^|vj|UAIR!D0`op%#$6jPQH(kUlP#@d4ZNt`0 z>+7wSepXRuW_Gsww1NF%tf>#9;<(#t9(?rJ@BHtNEG@NN8vSo;D@w&68v|sM)&O!Q z!dK+`x!CPxv=Aag7_0UBt8)wd{fvd>;+ZfquDtkMPOX(yc;<`8jxHWOymb5+7Eh-vjqA%+0mC{YBjR`903;5ZDIgoY7B@>fnKshs5e*Uw!!CLx;FL*KFDv$o=7}ln5Q3uhplE zubSjd6}Q$52*XU@j*J-K1PefIYE2P47l1ssYH3DM-@UDe!G|ZVLMHQvU;*f~S_`{W z)FsV6<=a;T92Yhy3D_6_dnxo}+bkAUl+2?Z=_(79&q_AiAg!`tUMiJg$y|h4%FdGI zTA&20h#m{~ELNI3pJJkUt`XrP2te2%7ZK-8#_~3qYcbJQ6(%VL#8a*kNaifS6&bu* zmE^4!!md3@F_{UF9wr|zYy)B=OPW*GW+v~4%1VMEV9m%vrWGau7$X8nhLXGi&Pd4K(BKfot+~{$tiVMwDoF_& z*@h^X%Zp^gQ}c7kppuea+hD>*W_w&qsB(>tD+-9Lzbuo4#7o6-#}$*%?nSYr4cGA7 za1_unECj4X5z6S@V5X5HR_Jyt{Y)UqY6-Fc6!LwLIgs@CEHYNw8dQaELfo+xlYp5te^nmJQ{S~C8nIgINF1t zQdT$&>ao=hiOwKNj@zfJ#B7_?7OGj7DjTeg>5|eDA&I%-6paL{vs=$wTinwSGSE@j zMC~8)XSftFYA7iC12&l&=&Nu8Q{$Lbt-PeV1htx83XuCSqfDOwRk7$~KUCzz<~d5u zQQ!nXMhFMk3L6uQL8zaKfpZLC0$5j$E_(M}8t5^AB^5{^fVlyOX164jU?yY#6RNDqSl`ZHdN2fw&lIYS2Qoo3)q%1J~eY43sMp zqnW9=u8FRa86`eY$s(YY!@xT&^CGSms-j{kP?4=|jv$&KLfWcJe zuxLspIr3P@nggV`4-~9Wgt+8viD13v38mb0A1N~dY>+`xpZ*x+E?QXGxpEL_Ad?*7 z)zx}q+d12=xb&RynVH(US)NflUG_59_!yrk&M(v^CV3beZylU(|KT5f?9RLQNDuAF zdP92#_+V+U=mZ8+!;^qit63lCZ!Oa6f)7@Q4=>@QHBd>^YRm23-a`v}j&|o3x;blR zdg|rRc^dYnKCud--RmtccNZ7hOUs>gv(uY4O=C)R+TG=4f3-3Pns2UYruQ&K`K?^t zWquTQwTTXFj5YP2uIf|FO5SS>nHZB0vAcrX@@li*s;^%^F*Vg1=f+tdhkx1N7p>#y z@y_e++I5Iub>q^H0o3oQ9}L37CzDupfvyhL1_~gk2{hLZjBmUs%3)UPk*yt!G?|J` zlPR%s$hU5EA7VfrGy}3=K(|im6*51C{HP&&p(^bA8)%*$O0KnTS&<0G7xW zDda`5U;*%Lwk=_`iO$H{fD$`^>S&l7n`oGk0|Z=!jo}ljvYpA=BAEgJGi)0+Gx~}O z+z+@i)EiRe(WddGCIcX{S|_7*s?k7JVGPrZqecZ6F({TwdY~Z&xTVx1B@EV?IJs)b zxTARZRbEzTAX#Y3aqeF2VbJ4dhOj!^Oq*1M#PkHtAgy2maVaxw4!c~18UjF%6_(`< z7!&rCgY?*PPYhO=G-NRpqz3CzoZ+O*ENYKnzVi?gkcGBmfY|($S;`kujkMV_)}5@W zRkp_{ipz2*P2{XBPa^eLMixuAT|yOM3lIl1(mdRwQMgzQf(243c31d{6enLLc%cj% z4T=dTQzRaNj^SxSb7a+XgfUMU(P0FuCn726BqEl%GLN)X^j3O{{l&%p+~Uf@5`QX# zzmstSU(r@4rmm4>Ufw4(IFVgyF9NDH0Z!0a!w}S4q0ok#u7Di&t*#*{g=#<;L9j0K zoz7r-Kxq{a$U!e6UDtxD3!tSrlo7SAa*O4A+4^#5F;y976epF|oLk3C6Gqi=BpeFR z45`B_vfWE53}a-Ld(VBxAw{dmbrQmu;JyI|1{B){8#5Btty~I~Dxjmwf)0*KPzRqJ zA$VoToRjetqkO2SpyUWG1w5Y|C>UGnU?C;Utne$y`Q>oRC>2tF zq%fE{7>ogCDvFT7HCrDWuh-j+`jO+yw}1Z5J&znXdR+gg8AVn*E0|h*Qcc9kH6U}c zb~2JbsY#4V46<2a#3jOG_*8Yhd{7je2;Z)N@6ODAouks3?JoS6Am3JB#g=x_);#ew+sc7#*D=;IK9ut*KUH4~Xveit6f@-(K6i|s(bw1vtZoG*oRAMt3k8Vz`g>>2eAB7#z@yAB>_alx8S6_j3c zS^*lNk#Hf`7TYH*aZna)lkJp1t0*8kBbUnOEE5h8!~pta4A@abY`z0qFUe?vYeaFK zaiIh>I1G~KXhJs)e(b9V_#k844WtAH?>6pUINTMq>!9GVsMssR09 zM-i14k|GSyHVJ_S%fSFqh7B_>7tD?I4pJ>au#|fsX)u$usGk8gL~+PS$Zcm9V$UM= z7?~vlGGRDr0ct~EB?55L!5m6T;Yd(E+wUeIq^V10Ndx)nH1iFcQHGP?qb;7FZYK38 zD_w$v;F;_|`NS_^^K4a`d)b(>JqC+Q1!5?4=a)5Fz8n*(+Jwsb4w2G^oxmt*Cbu;` zRJ2fOLhCBS@nS6Dtc`>c+-p>tI)dOuTQ;951%aS_+cX5QJLiK2b*gL`JZErbrLC9U z2(EsWOTWB7zqB&9#CUPpzv;Bv);D>CoqR?Ls)}&WSsGx?Bss&0EXP`Vo>~-3B5U=n(vLx|ZkPHL5meHU9g^dNQ?06CoTm02JXyjx;{VhiG%mT)BI7Z_M(!2HhWXSj<}Kxp>mgIv4-y9 ze1xVC5hNgA_btHu)z!~kf7ko|>I>YEQAGRjV@7T1CzC>D zj^w(fGshH>Sd`Ln76vF&FC}wF8n((ZVN&KuGnG?L0(^KZ^B`jZWkMfxy7PG8PA-%A zh;0&L1RP14`u{^%$WjVrlTf>r&okL?5+r4>{jHyO=tQtV7~)^ zoUl`-B1V!Lh6374VF_e_=^^8&A>mFDK~k;2cAPy)8-q~NL8Z2VniWjDjfk@_G`I@e zki-iP5>W0gLXTOT#-leA_5-25ksfgM*G;kD`^vnAT%l!p9iu?xDPES9p*k9%8Mb-I5 z6I7IhRirA)F)n#PFg})HC=ZAUWjL4Rd=cD^A~;!P4vD#M87pPYZgQbI)fG)BFpwNZ zlw{$OaI#}<$O0o8o(3+!x$ID@jW@>-S`GaxODY2O-np@nfCjx#4A~V-y&fqja))2r0h=*vIl~Bjiemj8Q-? z8*K22sp>R_8zvU=xi)pbgl&1k%hV_yQ9%nSA6MTqxn`}!0zjc7Ard8CAunMjL9jJy zk(lu^f~sB&)yFG?MpMrZ6Cq9nMtO1!1R*X`RS*hhR6$;NrDL@|;^^22ezsBOBxMyz zCDrsI6k-l1$3Ovuk*LbBrYgpK>AaVhQ%8G&P#uz;P!od0Yx$`eN`^=r2Cnxwo32@eM6HW!R z=~O_eLCKgTMov_?0W`Fd5vjCMxf^&o9RJb-H@ffzBa3Mt*ZDB*#zzf1(CXAivO=Bi~)EA#i`;aSKZYCv7$lM*@OI z^AN`E3pJ$Wgbb4zuucYYebyWG*0?`f3vOXKaBQioj!<8eI;2FN2oNi^mrycl7#cYP zdAX43GRI*3Xrj!>cVNhXVbiz`D=vIRbqQ*=JP-%m(p}mo<6IZJ%>|{@{G1CSs#ae@ zq>vPcOaw8rs_dINP+kGrtPrI@A&VXnWW-0n%Y*~;boH@MXaigjG3I>^bQHy5ZjzCK zVGrj}>LMEnz{70ueQ(B5*1=Wd#q!hx5^Y|>XIMXSDK9bWu{=Nl%^dpV>Z@!{D3@qv zGRDxSkbx|QQWv>sQb4fZhO)|`fQ%dlnYJGSrHt#G>Fl$ z&Mpp-I^^_cb;@#eE~S>Kl7K5l0pInd05LUyKthsCDNR)rFlksVW7QHIej0=X ztK)qErw4hmQ5Lt2W&V^Rzw&euLBFX8qTAE20`P|cRH;m|F)o-aBR`manVGH7G_e3B zlQ2Ra2cXC;F==)576aSiq%?e`IX`3cvZFJC_3QA&DDVAu)2zw&h11)nx{XD#9Jl$@Q->ULO=;tkjyuijHven zumEKwmWou0Wy(^_B%#TelDX<4pa-H{fa8%miv5!~zN*k;0EhR42)UCw#48FxrCudM61oHlM5=VDAjV&lvl!MK zls(9dq6unxTula%_`a0jeBL?|ffybm&$KDP6vSX=z8y#hv={?{fSbMboFMSDtl)Zc zC{aVmStEmCBXUyU+dz%_{OanS`}W=arF+^-OAAX&7hY2^p7=MYgvfoc&&zfMOH;ceArtxI)i03MtF6){UdYBM;3ZV7P`K%Tj})V#8sDGsNd4W zy&K?L4iN6sW8_z+qZ-^u=t_bIe{z)%75h39#3EtPOvMp~DiP_h;3eUln1_KdHvU%v zp#3vb&1h9H11R<2!DHxR6)z&AM6L;R0>o$rFTn(u)bU^lFac)p4p|CiyRst09E21g zAB!(^YND-^>Jy{j8F4gCMt zBFmZ`Y=CVrq>S|r^XW-)E|AW~!#VW-!Pw37vq9wMT3A-~x@kGsB*fT2;3YCNV`fKC z?N(w9D1j!^S?!ZB$5|C{LWKfh-J#(aci6 z0W7ZIb(+nf5@|t%k)aZ6PNUBSnARZGW(!q{S4-v~&JYp{DuU3Tz**a z0tsSD*dZ0SAYa(E#bE5ZDP?&iz;Q^B4)%;42%wNhkR&49E$k>FEO%G+IB})Rql*5u z0h3Wxx-4E&8~TIFS6h;jG)E@o!HHl2&xn)410^L9l=PVAq#MYDk`WoKR5iQS1hd2( zMp1eYA+Kf_A-i@FAyr3sr}s23!%GdO>+Lu`S{!eT`ED&?9_-vo&wAgixDEz_^;jt` zgQj6#aQrEg!dg&n)+J?@7iXYc7IwbNWf`{>u>;%1u3*O~hhXn8j!w z32Zlm-m!C_Kbgd=>QqAZ8zYol(@KbX*G8^eK`TFU0Dj-tc&lA+Jo?a~FMsB?0|$=G z%`Yu?yIBMi35O>I5>h}N8Q>_6;ClpS5gZSC01)NZQF4gl_YcbOeB(!m>(*n;wU>6B zzU89xPMe$<7lT=4ffHg-EQUKn%1%X>gi4bsAy_E`SItC21S2k1Nk*nic?oBRWgLJO zzxGG}B$Zi!OJG4Z8lSu2t~>7Ahm`NAAaof8YIj*7oj3~UW2o5VwR7Cn zMLkZ9ygkIS>+>LBQk$B|RPErYko4*pzu0Di?xXH6>L+BlpH^!R9h`$62B%YGfGnj( zLtC2Qq&OmVt}K8WVZ!hoGMik~rVGUARLI`25iKtJig6!K!gimaP8)2Lh)uo0T+Y@UU&{=u-9tG)xBN7dIczlJvOPV*{whb z!xTd-8X|wSf~4QN%em|s*}z6+zolATbeHFVtCK=9ldzdsPN2hP*^i`S21%mN{QwrO zb+Hj3qjDus2|HY_wav=_=>U|Kv$#|QlVnJi7jy>4Hwk5hKxX3-gpnqXW8~!`W>Q!M zsBO4D0qht-C9R4*fr6<>xo8N>LRO&wI0jIhD^wv1Akcu+u0n}}nQ29UlavK0kR)^0 zAqlFGL@kVPL_l@g0+}SOY!$MjsNthN@)Rjogs%=dE;F_`SReWMsG z8m6wbWJ;bC(7+{?J3BMkt{jYlQ-#JuHPW_(;1rAmat2ilf`vs;P{NFcb(O&C0vdD% znN+}w%!GYK#NdQHzh}Q_9d7=Z5+I`W%CJC$oRqxM3Zl!oh}QzMlszmmTrdk#v0mwI z3Ky@uTao6oI)SlB#?Fl_F#@cyd>w^euZ!~#2-J#`A7Uw86E2kz+#y&M1faaz>sdIP z0WFwg47QxS!bm2g9@Qs0A_;47V~E{w^M$ykq^YXiO5_qYI9&_DM8(ziX_y!yQOZ%e zlIhJ)w>a&B++T)0CRUxPNSQHA1j6=mrnliTf#BRVAdxIO5FQJ1Ayl2ip!b*p+Fx-a zL&qABUQHCy!gy_UV!XN3Y#cj0|Gc^3rm<)4|MDC;J*mgX&opD)P_+ck;9@%U{h7+&uu#`0?K+zZa$zHxSHf}k9-kil#v=1 zla&s`umDlbMkV7dK3b#>{YVlYEtbSb0@WU$TRt@3IXK_#_f}9KO{KBR&O3W@d|V$g z1?UU%iK5bU4{@sGZ;Fi3aqRaye8#w1>-keFZmI24J2*kPA@QB!RDm*raO{2{NSS7* zPfh8k9l_HWkR~KntLY~+SUUACqTJ7cYN~(~SP_G*4#R~s<_2z`{w@{>BL@M5s*7Mf zARq(Z#_k`IYB%uLHWlBQ8F1>GnSyxZrFs@NrXQMlm)w?T#lG@-j8b;_wn3baziP*Lf-%3k4y* z+>W{I3w4D<(#V-yq$pT9){gAD5~OJj88@U1RShrEak7W}P{HaMq916vgS4qpLKaL= zdFeVKmY0_L{cz`S7JEz{W2A?GtY%aHV8oB4^_Q0xB@Qr?Ym>I?RRPoK#Y>51N@a_k zNmC;Pl9alkR7g~jB1sVxRECfU36!8w&_Fnd5qUXLFaXXI2Y!F}* z6g_O1L8%nHG!&1nri#=7CzJbI!ZpCE69n{5xQ$Wn{oW6Jfe9B8a7B&%!Qb4ZkwkrL75kLl4vgH9fbjf6`w+k3roX5k zTC_3{XP>tHjO|`i@f>RLA?gwbAvZPy>?X!Fch|bl4=_(Mi zU*sAcLRv9WS=hx9oJq}ziK&zi>l_&yBT44U386PqvXJ)3BwT_{!v5SQ=+ulLtP^y_ zQ~_+;-~cbt9YrP19tpLJ-y)m5#JC@Ne=}TZsvQc^Kz;^QnSd-(Db3Ri2-61n$&@LX zT`12eaX>=AeU6ge5m0Z@2wCC)WSun6!Z40n7;w_I%@p>WQC&(gc)W^&2{wZvDMRa! zv}?_PUP@UG1yU)JDI;qG(wa^(DFm&yTR0aVfSMjL?N7nT6&cn;RcmMiqB{49RJnUy z1d&%tUM~#F_dr-Ut4A`FIBg}VpzCsf(y9c|aDcoj%}))g068v%TnhMt!Hsl+X!6YG zX63ivwDVDmOi12cr7WRJm;?A^EH+U5eq(0x!p4pA6>~_K8I_YvEhZ45f;6Cm{josxyV@#0RQOlxGrk0rxWq%;Kt=&B!@KCQbm9we8d) zJxXi*`SB{X&g?`~fl6q8s;j6oHL7S`fXUIi0uW$OOJq!h0!~y|086N2Q809fKPU&R z7QP~aU5HeUx#(@rcW`vcm;t0 z@=6W9B`Hiq6PQ+p^1K{GkVR=_0W4oJ!|Y*<%^y43ZFeFuHa0Oa!B3Y_|7cheu#v$n zp{xYxcG`2tj`8HA^;@~CMcbVsJH2=@jTo)NhQ#LcM5YY#B^Sa81H43)rJ^8+YFlJU zVa*^?AzuJcOmM^UJdtOpH^1yClA|Q|mvYGf43RD6i~*scGJuK|Pv?rqbPgeP0^|<( z`T&(XU|~9rP=4yDtU`!&4&FR)vbv%fNI@fbe2_>EcVzK6A&G@rhPwA4cY`DFOBx z8IK*D(F1cD`-~fHE(Lro5-z~@^TJ3urbwu7D8_e}cup>z*%I!&Z{MAF?}i|EHmSsm zU0iK?(Nt2dk^ogC;49KEMLnu|Y?TfNmH$`5|5T(7*xOrK?O^lbQM0gG%MeKl#m0KD zLy|>0Xo3>cvFv7`LV=eM7B|@9XRsKna(4C=)fneHnF9LT@d_Qq4*w6HkEbk55ld4B zlv#d7TqK8(rOd!jgbgd6xASB6|N2% zxPyrxeL_f>LTd^Zbrpgf899nN{0QPpR_1A?=KcsBfs|#wn!}QeP8E+n{NUnz`~b88 zXK#Fb9ER|n_91NEy!nN~bD#TM)Ery^NgN+1CnuZD_=-qiVSaA!BfFB3BOq@1g-Fug z=i)}>Ma^6Z=oP}g;u2SMUkpJ(3DO#5BFdnGT!#=0^5T*y`+>B;){0*Q<;^LnTaUigN6fY?fZ_2}I4k@@bSIextjItKv+e8sCT zzpT^kb-R83=tYmeuP9%U?*YSajQ>95dj~b}x*X{|32=kx>z!%_O&0`->r-yHBEui= z1KdCcn{FU23mKs~@D>Rsg2#l;dn>DFuN?c?3n!GJW0Bs`V8x&bUEQ{XYKj%9_%O(A zY~Q{e7tCYm{mIK)WTFLZij?rylmdM}1=rx_Lw5*RVS=kAN9)fPmF)tPv0jKRgWw`G zLx)}8vBf2D4b7w?nF;G}ygQbCl5+JtVN_7$~vP(JF5Trx^EDu;O_5UUIg)@%e}NdOUy zVNgf;iK1O=LYfstoCgR29CugdxKJ4`RR%cGF6EI>Y*=k(<`^~ux%M(#R?!KOe3n>u zPDmGrI6d(rRAUgPs%@l6$u{Q}bcQ=$2*UlWuFePr17!hMASTSsK`W3Gfot zIEzE!4ELLRN*faiUDXIB2O*IVD1$oj0#nsxNHQEn6VjkR$y%O<#a|S+7-hsRn8gJM zDhWtv2>sUh((LTq8INnttef4iDf@V|M*xw=AQZJ)t*fuT`e_O0pMO4>5Cc#gP9|JU z5J;*}t=(?#efYtB58gd@-%aKWAe-kGlD1idbD5DOh`6E#Rg$i>gF(pUOypJw)s#@F zAfd!7Wvya+=B#im7cp!o_S34c5G$_NuBNGkB^Pr^pyuc{+U}K zMGaA!aF2*2N~RH(1Ylxn8GXfxRzuIT$mQ{nhs@-ojskKJjvik=IM+FFyax{uNmPw^ zc5>p1i_V{!n$ibM`STe1jyjsnJ9{{XeJ~;4YH%y$U0-iez)dTCq{*`s+W`Ni5yx*t zSX~1*eL4hc4&m0J&9Yq$tWQmiH=BG$TW{1G2--gE0qo2I94KFaTN zD!QME+?9h@11WYIuZ3t8n!A-9kuB9Wff61EbhjNFx<=u}GPaEM2f{oj2*_BQgF%j% z&|_5&0TjnUUy}(j#wJ(6>R?Z$hESXJha_H7?z+s$*1BDwYyg|=B13SEE;UFiE}-)U z0?ot4txmNE$WqYMz@+wPP?;ls&X-7no||}e^_1bN3mXus1tUvzX0od_d;`8>4iJkD zNoL#r%mL3jaiQ9j1!hbY)jJI8&jMx3QI?7rTvyf^Au@wty(}5MhX5i2W!X!PApta~Qs3^&lhC@Bc!tPIv7orPWr zK_S#u78GD%i>Zx-Vz)XJsO^pk7f~Ls77`!>{j?wTB{;t#Y5jwuVQB^B zJLp`J`UwoObV(=@&ZU5oslBE46{x6K1FlyMOGG(HrraZ8`>8>~15FXg8EJAtQG|Ad z>pDXHr!zraA9Ag5KgLX4RJbFRSv{dSnLSDNP-(7a~(U~b7 zN#-PkK%@*RwMum(8A?i$<&$m}XO6@KE=0tc<}xvZJn==S0`vV+!W;>WM6cC4xc#(; z&N^q`rfrLd9yzq{k)ubCVsb||x4=3)L->A^laq)u29m%4r|c_2A=Dn)`^e({2i;P! z3&=dZU~c}*gbtk1oHbzyQ@*NzTFks?y)TguR8s`yU8n>x%`37vC+QsWp;!KtX{kvP zOX5W_AQCjEnvt_g7)T11@`H<9Vw7M7Cl&ZchT2;VdfwsCW zeFu*&yzj#|N}4sAS;}7nQ6C}wK}^7Tf`3I#aDMqOi%OhG8M5aVI)~>GddnUD%c4gJ zhMs-e_S3g)2Howhbo7m<^alI-$xbA3gutDi9w79OHJU9>22KnHObLv)4`MjBBc?b6 zu`(6)s%^v(lD?w*HaAWL&u9!|jTYQPdi6d|;)T{C!CYMH_1X43bbjZ$rB|N4PIoy? ze_F>Wnhr)ttLAd`!VqH+|Nr8?tvAx+Opo0pyLlK6XE>*oEGf>~UL=;+apHx61OamR z4fz-QN3xemE&>F|O>Tl9H#uG;K$5Ekb~nIg<7{kMSzEFsOBxNynIW5pJ~GeYeXHv0 zCTB(?DS233_11f?`i|9Kvzt#BwXbtlfB)UhFkw1*2kcv6)R15TTyrxp3Bo~m7~=e` zuBMMnR3UYfQq5N@I2-haz$&=+H@)RVXq+sS+k9M`Ul7A_S%2t1gB{=~C24 zO`xW?G$hTpGD1w}QX@=hL>!bqfSO;pY7wiPiZGN26{do-q`eIKEZ(Pt^c@BI9!pUH z2YfFSy>(uIaz)El`f?F7fZ854b%66>DK=fuP-&Z}1wQ2mzyLjb^te};BXgo_7z@QF zB3kJ4ErxuffzNkDSXw=(%di;^U2ilRZ1-}n2{#d+sh~fRH4AB^Y+s8iXBd>W*0@F zP;T3zS8M{+JTGytXKS{V93*P!im-%D1A4MhfD_imAt8YPhce=s*OQ_U+)ep0)btOe z5w&P!O$l+q;6p_?=3Zn-x2>(uMzy$jr7(aw20~_`qJ$AxjX^^aMP~`zs*2812)Q-b zCQW&z$!~j0@RYWBgfclVuoH?Lgy|bCoP=uKf}3&v$Pf-Ft9!_x}0%=4`f@@`iUYAMCB~9_;S!`ZK6j27#LuEdQ`> z1Rw$mQD7;+cgWKgK+FTio}X|bwjOkgzB)-XqbuB z;Vt9ForY|@()|sI+=Q3L{fb~Zj!|pyRIqL82mi2`Twk9ap6=Y)+26m#FAm`|sP1TN zh*z@3h>j#F;1R&ubfK1^16jBC6@9F5uGT;O{17XU^g~3TqDxy7!O8}Gyc#Ag#qH{n zR^J`_xcH7+^B80x;hSzOq z`!p3$eBKf)c#2kQw&Gz*wOsU#B!Ps*r=jn~a0wd@Hu&I_A9_aPM~j>K+%2{ox?79s zgVV=8vfmv}*0%QEJDJE0YY{ zatIFK>4BQNC5Y2U1tOee*EPd+qxd!yUGOPF$p<(vp@KZT6cIwS{WvQ#?S#&oY?r_s zvH&)O+sI0WPR|*3AZ;j<6af%(Z zAWs6O8r=e~5?!$KaJk4Er;tL?#q+}uIdw$@Z)H9KXu1GqS{i*#NZBW0?;1KW)!IXl zsQH%L`P#EmboNLCFhKwOF9OJ5&~yGopsr$ zuRkV3wNjg-ngIx%PhDcI$jlYPA*@9^xm2NT*c1Htiy0+~8dmZ|BU?)|y#|)LRLPX2 z9zd5zfcWlO6Xxa=m|sA}w%b((n5}YM$^^`A3O55 zaq>AhxV^ZXe)h%Fj!*k&9EGGR1n911>ZQSSbV;l44r(r}&|{w!FMmPPp+)hRHeM^1 zE?=UO5u7k8Tuj&1+JiUm-@3E6HXO3gk9}Po9-sWffAsIEM7>l6I1wW3B6|co!DQ{5 zFFjYpNuK8;Wr#fMFK1X|1yGw$7g#GEpUxhiOu_MHhuQ6&?Z5fM9~>T!CYOtgizy!< zVmmFauwsPW#$Xf2_yp_)KZy$F&Tw-&o#GUU8vwYapY{xZKF%|G?>pYGP$VkoqRcOs z)J-n=tYxb7S#gZLt*rq+fXYvz@~0O!(DHXIxg06i0+?TvvO@C>y+&Apu9>CahSiBVFm@0y@uIY?U36GF zkfVUZOSheaTr^u@p=oE0I(hph@ zsp;Esk2ghL-gsypJ8GMgi50aL9*orwj+IM+h4w9aHrzr)CnAjCr@4WM;In941>spK zS;rtDgTgNyQQ0*^I-pF$DJ2Sz4aBi$h9#x&DMKz@G-D`b_UIw?=_DU2daZ~W1EdbU zxQb%ylDV6%%RPMuh>}Lj^JZsx!9vrs&aS}=)|syNMh^QumH^1 zc6c?e(B&Lz-vB#`eAXgOG(oH&235{m5upj%9Dw0p3II)305!ZQQ37mb5+x~A1hr9d zC^$27-DQ?lc!IJK)x*R-S}V#!FoXhEe;;mEcL6v?<=MXL!EWf#+I&BS2Ebj37Gt6l}!Rix=@ z;I0Rtc_mSi1G~ZZb$F4gWOl}}VB2Ds;7lP7L+ki+X*0ndy{Vt$N6oipb*Q1$)!!$N zPTul_lBp;{GFe_7aDi}4IkFba>4N*@ifh?&sNo|>{4Iltc^-oCIw+455AX1CB9@5L z8NWAju6IA^8-YQAvAI1P+&(=$-P_x1KT!G_SS%Lj=jZRg|NhgbPftJk-{-&n>2&lQ zd`0A-F|4vdU3--IV$-lotLbRyE+HDP>?E$)3gUFQ=EyWd2_6`FL<^ zxO;ndou6aW-H(no$<>nNKAs*xhSU<%)-kU(OMna7`=U{C)n7ZGBIKjR=%R_99W{SdLI2f^i}{=H zJlxs8y*3zd9<*Hi&;RwO@BPJRR0~a-c$NhFHchyXy5Uh_lmD`aRTD>q7YMATY!fAZJn)2mM&KRY=e&xxxkzZGe5IpI(9PsS5C#X{|J zg5KJ#9jsM*a>0+A;(VoF?Vx`iARf-OYegrIl$(1l36wJ@k(&YJU=WWMxirLDQI8g} zOvTa>TbjJ}{=w*Q_p2W+`w91*+f$sqZXJi1w0z=4y1&W)xV3H%Czkg?xYmDKn9u5WfH= zx_Zni69Xrh$v2JH3%r)W0>~!{_DvNKbnC%-6lLvaz6g)knNZ(FCZ1scQs_XVc(Wn- zI+20nQavbt7eWmx@Tr+PW||*;o(05IN~94bBm}f5a&*r%cdw;YTLNN zknlpW>|HL<@kEtT^Tg$nd|A^qr%1!J6BZnlU?rSIjb2JJ83i4s#OUHXv^N|jk`f}E z1GBOdu(W@2rK%ht#V+9jk)lzv&?JJAH)(qNM#K^`bFjWsLWqzitP^|%A{3>R;Hb@} zgP7s=yk*a#O_Wj{fKpj9(2bDPI%eo*UGpl}Qtw<%Rw#q=wlZGMyD8>HT@g1$0dA)O; zY6`O&Bb62hpI!U*cbOqS zg(5$6N?L_P(e2o@ZrdEXN~P_vUx1YzsrH>yP_Knf%F!(Z+p18h0!GE_8p(B79mqEZ zXM}(yHH4nb*820={=@yP?IACII@Ba{&5CsGu_{@|0hvVYx>*Hw8=i=GuoN8-!>z$q zfBoZ6kNkMbOjeB}^4V>ZA((UUx9;6t+t^@dzBoKS`N#k4KS3lNUP-xyXcoL^Vhy*qGk`b#eg>{6S~GbZuJA$^JYJz_L;+wK){4j+c}l-n zC=YEemEP8HfPvvEhLs{8E%JVapG4*AvEN6FNq+e3lfjJ75?lOudwhF)`_G>9$8zgL zaq?C+> z0zfAUUJa;_cQ+8Zke^P>N1(#@Ot!@?m+Dr*u!!B_*+oOZuE{{+8_+SwBQ1#0r4t)A z@^EXA&T+Bin=LTsewCAEplv4>#>Ft0UdbfnDdvRteJ+_EP%<&WQP?3`*DBTsCITf> zJ}Ru>)f8M6WeA*r$HJ4Amw)oYN}AWwRO3mG%CKco^3ES%Hwhm{JmsWRUeY8PQIsmP z&GD!T6bE6I%G8Hmii>APG!c?zw9WylwJDWRFX>g-H>0WH$v7-{9>er#%G>1R%q+ar zgdiXe9nu-?Y)Qv7uq|G^u-K1mpvBrtM450!Q97mKWr>bp%Zo>Ot|$e->PsN6cu?z0 zNY%|jL_Rq(cdb^HyP%>pIakFk#;b1BRu$+|`XpWrRIJLh(*(1Fdc{wp>ZVP7S=ng0 zpj||>W!)(pc;Mp)j+yFtdo1uHw6!{Lfa*F zOZ8&(AeQEtIqUDsriWGH$^PBrJNKU4y??eo&=_r$1D!)62&n z%rEsD{TyVp0$a~TWI`-&5Kn@0tVHw*il}0h#a_Murc+jls6{gyA}!Tsr%;vKGP+pN zc^xR7Cau6ycLgtqNKwfu^}4%8r}K(fPH@G?((e7W!;|E&Ah0@HJ0IUZ*xlOQ-W>4x zjUM+a>x|y*KSkhH?9TvSSU2&*tP1Il>+}WR9Tz>ZCxxGu&#?j2U+l>;w z#3-+DLb|Yg7gRbt6H<#LE3H8&(v>ZpieuuMM~m5&eoB@}VzGAh#ybyp4({^jjBw_i zFaG7f{=<(xsUMz%2ONi5=FvL1^iVtSRs{mOyF>oVVoYiOieCq#_&Q2uT=uavMvU`| z$&-`u(dqQ*`8>Gd|CPV=){p<{_bx8Sj~?FH+ueF}e-Dc>E*WvnSko8Y@ShbKfP=VN zyS>dHt{84@=>HZGfx)P%$=2?}aq+ogNy8UqtQDcd-yimgYVJE&RKXnK}lwEafGg3eEwmR%{uDa4c)8E;InbM>?4q1+L&9 zoD#)oy#Y?6Y)|lwVin$0>>M@6>yJTdhYKf-l|z{5Kz&aUXr_@40SuM`a3lrHus#a5 zJL&>EFCi?oaxm@65M3f-#&~$P3bHCAMJcdP-(*>#s>!P8MmIf#h-uLlljcLss+Qu& zK;bvXsGp~-SWS_s1X{>nvsSFngCv#-+a}cxoST>=a{r-QjnuG$&F#uoRL->`H<{i| za49-cQN2W$wW5aUDdmRC=~kiwa6!=A}Q0kWpx%vSmUNUSOOB zn^-a0fjB|fm>apzkJ)go$i^ymz-WOaid;JK?5(P-T78p>!grdBVhIi}U%#_;fVFA`lpjMkg=C^XJdKRs>E@o?nih zO^!c3``~|`fBOFX^0bND9jE7l2c5Z{4Z;bC4c8FSDhI&tv$~F*I?{Aw}oJv#5V^Hd1ctK!M|3(rAGsT{%7;a^Yepm9c&IZ z`LhHyWU~_%rVhi>&mGggq*0U#P^ULYGxKB(>O(2ERfjM7SG}n*OXQ0t(h_Yc#A{-NvdY;_tz49qise z;LXP3>hQ_)fBrB23j_t&A{@1}4f%mxf^_Wx=xh)4FN?O+f))!XNo-$tV8U!{@agDul`z+0 z!^~2nK<2IFObRHmE8nT70xt>$RBh*hGQH>(HK!jzT!f9;9A`=nNEZA<9FP)TLBw zCl}D=N=+wOJg#W zX>+@Mz;{Y{2c*@49i|hI|z)@%*)j?9ri8x{1c~>E8yj(<+Oisx=IM(3B7zE<_$O}RRbODSu zxOa1vxDm@m)YQQxPNmUSf?Y;J(L_ZKUnAf*#8akI!dk&&Q8HJbC|5E4bMekg zH!$nHD=ewxAWaEwXZQJs-qqESe`WC$=fAqw?r(4X-n+lEzSh6IoP2RSI(~M3ayEW) zbaMFg?B3m52X}8BKRd&UQ41X`2w{rlBA=AvQi2=?1HA*lhh_W_tNkJcT%^!p0vQTS zDB|)&Ze&(=vpQ^UJ}xOYHuM7|Sg+z5wt=EOhadmfTN}pC?Bd|8KB`^CcK_<{-0dAs z*FL;t7rq?=4mMEa$KNCXY@4uu@N>_lQz zC8ZLpx)l@LHa+LyT0*`^gmDbV_4UyC-o-}1GNERP*>;_qOBLiSIkYDbC2|w2&e9B% z3MD6bhzYlEDbN)bids8SM>Rwa;V7PGx{zkA9J(^0k%YZfO0)p)GkTQqV3jQD~Ku;o77EY zW+cThS~b&glq6{7DUkwoq=&9W_Gy&c5Hc-w|H}a5Vi6IL2YL&FjiupR2Be*ni3&4Q z6hJ5Yl=yalf(e(V; z`14;*k3U^Z&go2Fydbf>bPATGEd2WU@!90*=Vu@P?DQ9ZJo?rDn2nx#kyc8Z6CjSD z*>G#{*i)7=NUP`?B%!nf55I%>4gGkWXqx#{uT9%_Ob4e5l3GQ?UaC!Sgx#$SEOl?d zt7!o-i^PhJhU-+&Ch^T;HHl6`v?@*dUkTL>_N4jZ@@)LZ+xOP`>%+|rbDLUi238w= z?uEgu?aBs}dJEpr>0j)kPY(Wio4x(rTjwXE!>1$3Bu^#9D^y`KBy9^*FI3`Eq01^A zRJ>O(haYOBzTXQCJ+`UKM`1E)YwKmV!|&>%>0`OLb?+|7_kQ-#zx@w?LN58ZZ$bqn z`66MGl{(rB!%YR2ue(DmnsDLc{ZGH=e_6B^Yk7zw9%MYCpN+UyJUpFUOyaT(;CTP; zTW|c<-FxHlY&@A=T#PR#v&-3hHouxq7w4Db$#^>0?9ZosZ+MEuBG-z&L9dSs3VdqC zxIP?m&zVeTIC9KK$hd7#4S<8fpP>4Mg;+3pMW7L9mDek#An$1d#LEt%P*w0Ug`w9*wvD zQiBuDY0O;cOV=;Kff`0}0-`VhI!q_d7HUwSxtIv2y)TltNeifXEHErn2@?}ufXQ0!cPV=3LQiv4FbE0Mjc~sjGd%wu&JiHpdkr} zSfZzVRP3Y-!FgKKvkeh-%xSg)$1Zc%VaSRGU~;Xfg4@7|E-@>40B9&%bgcmA;aSBx zr6$q|^|Nc(gj>WP%1xhELk%bPjJk#6TG>vl1FaxhI*f=m4f{9Dk1axR){Ty%pxgSV zl_xotQGB6>h$gIpwpDAp1P7$3D>dI~Gd&}>tuzyzRRtP zMeJiilQC@&c`M1>@OJS7Mc35}bMI+JN^TIFbAR75(e{D7d|t^POOwD(Edh zzbG2f63FpL<1ct$9geSBukc!&a~I=H0`CPle!tr3fvR%${#6V+z$)?d*53JWdo9Wp?Nc{32%&Us>~JU3l50}0BC zbfI?H;97K*UdiNTSQ%t6lFwFjAzlwKMJZ?9tJ&4gZ2HFc-}=rE-nn~cAIDwwj=8|l z%&Y4aHXHJbwq>QM@#U#$9b`pPCS7JxY z8gn?<-TK|X@!jor-o~W=!5{pqpa0@F5CBX-1dcua31EpTjM4_6aC;jICZ6gUq<^)+ zv!!lkmUSzjJGz*B{_OI}^XbtUzx~C4!M7hi`2Ju1_GGr0PiI(_J$ktRe}4Jtv(b1w zUkv+vqw&tI!P{>hjLt8SaFbE)Mw;lms(s?rtuTrNVTzrMA#2__bYo16Xh&Hg5riG0S$UtGi{JJAe%bkWl%aCxk(KYH{Cjn5H$ zx~SRLUghE;au`7sg4yT<6{i+3QOVW?w2!pNS|%n?SXn_jsFBZ{>vVQu&i9AmCm%i8 zr>$8#J^L|7HJrp9gt`VqIx>`1@G=>N2Ds}vVtNijIxU#3i$^E_E=)E3m_ungbl2(G zumI8mkjU019taAPY;-!1uW$$*lvFMj9U?Qa4QK?yU_H-mCfq2wbv%1c%%EYA0q1N5 zCJbAJum^x9lAoVuv(LgB0pkjfn$HzCqF|@sW@TyBg|3GJ2G6XdaIMythSM%l(`j|i3DSqv z^kJizq)a41Cv<~$S)_&HlYNkU-|G#4;2QtJgM6^kNSzyz9 zx_G$F7qijjgSYR^`AwUHZUCxjPJAIYkGr%Q3xcpP=|tP3Dvhzf)(4C-!ofD!+_<~H zw>F!7{Mk`#{Iso_8PPy2`YTMmP?38bD?3Ci7j?E|BMr7t(JhB9LMo$Z)r4{oR_`dV z5xKj!^XQFx>wA0e{ruy9^Y8x1g1S~z9lbKg{EN~8-JQWYPU)Ha75}LZf0iw)2XJ(c z{V$87>66pCRs?Phhd=t>_xQVs^ZC_cF`mrceDmIPIy*bNoXxo?0dUor&E^P%g%dx+ zinAQqXtB|d1N{%y*CF7~_v=K!+b*iqgvwe`lM+lIyYzx#SON7W)`;x;OK$jRQQw4$ z%b5qvl_Ect+Uvb_r0=`CA89fBRnV fhx`4X9Q6M$Vm40fkz1OC00000NkvXXu0mjfYPT~n literal 0 HcmV?d00001 diff --git a/docs/images/ex-blazor-app-simple.png b/docs/images/ex-blazor-app-simple.png new file mode 100644 index 0000000000000000000000000000000000000000..50851cbb0a14fdbdb01d47fe7a0589765b293f8c GIT binary patch literal 2753 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 literal 0 HcmV?d00001 diff --git a/docs/images/ex-console-app-full.png b/docs/images/ex-console-app-full.png new file mode 100644 index 0000000000000000000000000000000000000000..87e6aa8e6f4cde4c6e46ab547c60ffb5d454286a GIT binary patch literal 127026 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

+public enum GameRunState +{ + /// The game is not running. + NotRunning, + /// The game is running but no game has been started. + InMenu, + /// The game is running and a game has been loaded. + InGame +} + +/// Represents the current state of the Slime Rancher demo. +/// The current state of the game. +/// The current player state. +public record GameState(GameRunState State, PlayerState? Player); + +/// Represents the current state of the player in the Slime Rancher demo. +/// +/// We are using the StructLayout attribute with the "Explicit" value here, which allows us to specify the offset of +/// each field in the structure. This lets us to mimic the actual structure of the game's player state in memory, which +/// means we can read and write the player state directly from/to memory in a single operation. +/// +[StructLayout(LayoutKind.Explicit)] +public record struct PlayerState +{ + /// Current stamina of the player. + [FieldOffset(0x00)] + public float CurrentStamina; + + /// Current health of the player. + [FieldOffset(0x04)] + public float CurrentHealth; + + /// Current coin count of the player. + [FieldOffset(0x0C)] + public int CoinCount; + + /// Maximum health value, considering any upgrades. + [FieldOffset(0x40)] + public int MaxHealth; + + /// Maximum stamina value, considering any upgrades. + [FieldOffset(0x44)] + public int MaxStamina; +} \ No newline at end of file diff --git a/samples/MindControl.Samples.SlimeRancherDemo/MindControl.Samples.SlimeRancherDemo.csproj b/samples/MindControl.Samples.SlimeRancherDemo/MindControl.Samples.SlimeRancherDemo.csproj new file mode 100644 index 0000000..58117b0 --- /dev/null +++ b/samples/MindControl.Samples.SlimeRancherDemo/MindControl.Samples.SlimeRancherDemo.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/samples/MindControl.Samples.SlimeRancherDemo/SlimeRancherDemo.cs b/samples/MindControl.Samples.SlimeRancherDemo/SlimeRancherDemo.cs new file mode 100644 index 0000000..59ba981 --- /dev/null +++ b/samples/MindControl.Samples.SlimeRancherDemo/SlimeRancherDemo.cs @@ -0,0 +1,57 @@ +namespace MindControl.Samples.SlimeRancherDemo; + +/// +/// Represents the tracked game. Provides methods built on top of the MindControl classes to manipulate the game. +/// +public class SlimeRancherDemo +{ + private readonly PointerPath _playerStatePath = "UnityPlayer.dll+0168EEA0,8,100,28,20,74"; + private readonly ProcessTracker _processTracker = new("SlimeRancher"); + + private IDisposable? _infiniteStaminaFreezer; + + public bool IsInfinityStaminaEnabled => _infiniteStaminaFreezer != null; + + /// Gets the current game state. + public GameState GetGameState() + { + var process = _processTracker.GetProcessMemory(); + if (process == null) + return new GameState(GameRunState.NotRunning, Player: null); + + var playerStateResult = process.Read(_playerStatePath); + if (!playerStateResult.IsSuccess) + return new GameState(GameRunState.InMenu, Player: null); + + return new GameState(GameRunState.InGame, playerStateResult.Value); + } + + /// Sets the player state. + /// The new player state. + public bool SetPlayerState(PlayerState newState) + { + var process = _processTracker.GetProcessMemory(); + if (process == null) + return false; + + return process.Write(_playerStatePath, newState).IsSuccess; + } + + /// Freezes the stamina value to 100, or unfreezes it if it's already frozen. + public bool ToggleInfiniteStamina() + { + var process = _processTracker.GetProcessMemory(); + if (process == null) + return false; + + if (_infiniteStaminaFreezer != null) + { + _infiniteStaminaFreezer.Dispose(); + _infiniteStaminaFreezer = null; + } + else + _infiniteStaminaFreezer = process.GetAnchor(_playerStatePath).Freeze(100); + + return true; + } +} \ No newline at end of file diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/Components/App.razor b/samples/MindControl.Samples.SrDemoBlazorApp/Components/App.razor new file mode 100644 index 0000000..0c194f0 --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/Components/App.razor @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/Components/Components/GameStateComponent.razor b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Components/GameStateComponent.razor new file mode 100644 index 0000000..d1d7e9a --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Components/GameStateComponent.razor @@ -0,0 +1,68 @@ +@using MindControl.Samples.SlimeRancherDemo +@inject SlimeRancherDemoService SlimeRancherService + +@switch (CurrentGameState.State) +{ + case GameRunState.NotRunning: +

The game is not running.

+ break; + case GameRunState.InMenu: +

The game is running, but no game is currently loaded.

+ break; + case GameRunState.InGame: + default: +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + break; +} + +@code { + private GameState CurrentGameState => SlimeRancherService.CurrentGameState; + private PlayerState? PlayerState => SlimeRancherService.CurrentGameState.Player; + + protected override void OnInitialized() => SlimeRancherService.GameStateUpdated += OnGameStateUpdated; + private void OnGameStateUpdated(object? sender, GameState e) => InvokeAsync(StateHasChanged); + + private void OnStaminaModified(ChangeEventArgs e) + { + if (!int.TryParse(e.Value?.ToString(), out int stamina)) + return; + + var newPlayerState = PlayerState.GetValueOrDefault() with { CurrentStamina = stamina }; + SlimeRancherService.SetPlayerState(newPlayerState); + } + + private void OnHealthModified(ChangeEventArgs e) + { + if (!int.TryParse(e.Value?.ToString(), out int health)) + return; + + var newPlayerState = PlayerState.GetValueOrDefault() with { CurrentHealth = health }; + SlimeRancherService.SetPlayerState(newPlayerState); + } + + private void OnCoinsModified(ChangeEventArgs e) + { + if (!int.TryParse(e.Value?.ToString(), out int coins)) + return; + + var newPlayerState = PlayerState.GetValueOrDefault() with { CoinCount = coins }; + SlimeRancherService.SetPlayerState(newPlayerState); + } +} \ No newline at end of file diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/MainLayout.razor b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..7ff1bf0 --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/MainLayout.razor @@ -0,0 +1,19 @@ +@inherits LayoutComponentBase + +
+ + +
+
+ @Body +
+
+
+ +
\ No newline at end of file diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/MainLayout.razor.css b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..038baf1 --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/MainLayout.razor.css @@ -0,0 +1,96 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/NavMenu.razor b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/NavMenu.razor new file mode 100644 index 0000000..f4dd088 --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/NavMenu.razor @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/NavMenu.razor.css b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/NavMenu.razor.css new file mode 100644 index 0000000..4e15395 --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Layout/NavMenu.razor.css @@ -0,0 +1,105 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: white; + position: absolute; + top: 0.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); +} + +.navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep .nav-link { + color: #d7d7d7; + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep .nav-link:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +.nav-scrollable { + display: none; +} + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + /* Never collapse the sidebar for wide screens */ + display: block; + + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/Components/Pages/Error.razor b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Pages/Error.razor new file mode 100644 index 0000000..9d7c6be --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; + +} \ No newline at end of file diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/Components/Pages/Home.razor b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Pages/Home.razor new file mode 100644 index 0000000..aa13c71 --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Pages/Home.razor @@ -0,0 +1,15 @@ +@page "/" +@using MindControl.Samples.SrDemoBlazorApp.Components.Components + +Slime Rancher Demo MindControl Blazor App + +

Slime Rancher Demo MindControl Blazor App

+ +

+ This application is a demo for the MindControl library.
+ Start the free Slime Rancher demo to see and edit the game state.
+ Slime Rancher belongs to Monomi Park. They are not responsible for this software. +

+ +

Game state

+ \ No newline at end of file diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/Components/Routes.razor b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Routes.razor new file mode 100644 index 0000000..ae94e9e --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/Components/_Imports.razor b/samples/MindControl.Samples.SrDemoBlazorApp/Components/_Imports.razor new file mode 100644 index 0000000..f04aa6b --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/Components/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using MindControl.Samples.SrDemoBlazorApp +@using MindControl.Samples.SrDemoBlazorApp.Components \ No newline at end of file diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/MindControl.Samples.SrDemoBlazorApp.csproj b/samples/MindControl.Samples.SrDemoBlazorApp/MindControl.Samples.SrDemoBlazorApp.csproj new file mode 100644 index 0000000..23f110d --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/MindControl.Samples.SrDemoBlazorApp.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/Program.cs b/samples/MindControl.Samples.SrDemoBlazorApp/Program.cs new file mode 100644 index 0000000..3350ec2 --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/Program.cs @@ -0,0 +1,30 @@ +using MindControl.Samples.SrDemoBlazorApp; +using MindControl.Samples.SrDemoBlazorApp.Components; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services + // Register the SlimeRancherDemoService as a singleton, so it can be used by multiple pages and components. + .AddSingleton() + .AddRazorComponents() + .AddInteractiveServerComponents(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseStaticFiles(); +app.UseAntiforgery(); + +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); \ No newline at end of file diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/Properties/launchSettings.json b/samples/MindControl.Samples.SrDemoBlazorApp/Properties/launchSettings.json new file mode 100644 index 0000000..0bae6c8 --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:44869", + "sslPort": 44395 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5170", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7280;http://localhost:5170", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/SlimeRancherDemoService.cs b/samples/MindControl.Samples.SrDemoBlazorApp/SlimeRancherDemoService.cs new file mode 100644 index 0000000..b837464 --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/SlimeRancherDemoService.cs @@ -0,0 +1,40 @@ +using System.Timers; +using MindControl.Samples.SlimeRancherDemo; + +namespace MindControl.Samples.SrDemoBlazorApp; + +/// +/// A service built on top of the class, that periodically refreshes the game state. +/// This service is designed to be instanciated as a singleton and then used by multiple pages & components in the app. +/// +public class SlimeRancherDemoService +{ + private readonly SlimeRancherDemo.SlimeRancherDemo _slimeRancher; + private readonly System.Timers.Timer _stateUpdateTimer; + + /// Gets the current game state, refreshed periodically. + public GameState CurrentGameState { get; private set; } = new(GameRunState.NotRunning, null); + public event EventHandler? GameStateUpdated; + + public SlimeRancherDemoService() + { + _slimeRancher = new SlimeRancherDemo.SlimeRancherDemo(); + _stateUpdateTimer = new System.Timers.Timer(TimeSpan.FromMilliseconds(100)); + _stateUpdateTimer.Elapsed += OnStateUpdateTimerTick; + _stateUpdateTimer.Start(); + } + + /// Callback for the timer. Refreshes the game state. + private void OnStateUpdateTimerTick(object? sender, ElapsedEventArgs e) + { + CurrentGameState = _slimeRancher.GetGameState(); + GameStateUpdated?.Invoke(this, CurrentGameState); + } + + /// Writes the given player state to memory. + /// The player state to write. + public bool SetPlayerState(PlayerState playerState) => _slimeRancher.SetPlayerState(playerState); + + /// Enables or disables infinite stamina. + public void ToggleInfiniteStamina() => _slimeRancher.ToggleInfiniteStamina(); +} diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/appsettings.Development.json b/samples/MindControl.Samples.SrDemoBlazorApp/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/appsettings.json b/samples/MindControl.Samples.SrDemoBlazorApp/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/app.css b/samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/app.css new file mode 100644 index 0000000..72d1e34 --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/app.css @@ -0,0 +1,71 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +a, .btn-link { + color: #006bb7; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} + +.field-container { + display: flex; + flex-direction: column; + gap: 10px; +} + +.field { + display: flex; + align-items: center; + gap: 10px; +} + +.field label { + width: 100px; +} + +.field input { + width: 130px; +} \ No newline at end of file diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/bootstrap/bootstrap.min.css b/samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/bootstrap/bootstrap.min.css new file mode 100644 index 0000000..02ae65b --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/bootstrap/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.1.0 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-rgb:33,37,41;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/bootstrap/bootstrap.min.css.map b/samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/bootstrap/bootstrap.min.css.map new file mode 100644 index 0000000..afcd9e3 --- /dev/null +++ b/samples/MindControl.Samples.SrDemoBlazorApp/wwwroot/bootstrap/bootstrap.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","../../scss/mixins/_border-radius.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/_containers.scss","../../scss/mixins/_container.scss","../../scss/mixins/_breakpoints.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/_tables.scss","../../scss/mixins/_table-variants.scss","../../scss/forms/_labels.scss","../../scss/forms/_form-text.scss","../../scss/forms/_form-control.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_gradients.scss","../../scss/forms/_form-select.scss","../../scss/forms/_form-check.scss","../../scss/forms/_form-range.scss","../../scss/forms/_floating-labels.scss","../../scss/forms/_input-group.scss","../../scss/mixins/_forms.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/_button-group.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_accordion.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/mixins/_backdrop.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/_offcanvas.scss","../../scss/_placeholders.scss","../../scss/helpers/_colored-links.scss","../../scss/helpers/_ratio.scss","../../scss/helpers/_position.scss","../../scss/helpers/_stacks.scss","../../scss/helpers/_visually-hidden.scss","../../scss/mixins/_visually-hidden.scss","../../scss/helpers/_stretched-link.scss","../../scss/helpers/_text-truncation.scss","../../scss/mixins/_text-truncate.scss","../../scss/helpers/_vr.scss","../../scss/mixins/_utilities.scss","../../scss/utilities/_api.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap v5.1.0 (https://getbootstrap.com/)\n * Copyright 2011-2021 The Bootstrap Authors\n * Copyright 2011-2021 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\n\n// scss-docs-start import-stack\n// Configuration\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"utilities\";\n\n// Layout & components\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"containers\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"accordion\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"alert\";\n@import \"progress\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"offcanvas\";\n@import \"placeholders\";\n\n// Helpers\n@import \"helpers\";\n\n// Utilities\n@import \"utilities/api\";\n// scss-docs-end import-stack\n",":root {\n // Note: Custom variable values only support SassScript inside `#{}`.\n\n // Colors\n //\n // Generate palettes for full colors, grays, and theme colors.\n\n @each $color, $value in $colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $grays {\n --#{$variable-prefix}gray-#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$variable-prefix}#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors-rgb {\n --#{$variable-prefix}#{$color}-rgb: #{$value};\n }\n\n --#{$variable-prefix}white-rgb: #{to-rgb($white)};\n --#{$variable-prefix}black-rgb: #{to-rgb($black)};\n --#{$variable-prefix}body-rgb: #{to-rgb($body-color)};\n\n // Fonts\n\n // Note: Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --#{$variable-prefix}font-sans-serif: #{inspect($font-family-sans-serif)};\n --#{$variable-prefix}font-monospace: #{inspect($font-family-monospace)};\n --#{$variable-prefix}gradient: #{$gradient};\n\n // Root and body\n // stylelint-disable custom-property-empty-line-before\n // scss-docs-start root-body-variables\n @if $font-size-root != null {\n --#{$variable-prefix}root-font-size: #{$font-size-root};\n }\n --#{$variable-prefix}body-font-family: #{$font-family-base};\n --#{$variable-prefix}body-font-size: #{$font-size-base};\n --#{$variable-prefix}body-font-weight: #{$font-weight-base};\n --#{$variable-prefix}body-line-height: #{$line-height-base};\n --#{$variable-prefix}body-color: #{$body-color};\n @if $body-text-align != null {\n --#{$variable-prefix}body-text-align: #{$body-text-align};\n }\n --#{$variable-prefix}body-bg: #{$body-bg};\n // scss-docs-end root-body-variables\n // stylelint-enable custom-property-empty-line-before\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n\n// Root\n//\n// Ability to the value of the root font sizes, affecting the value of `rem`.\n// null by default, thus nothing is generated.\n\n:root {\n @if $font-size-root != null {\n font-size: var(--#{$variable-prefix}-root-font-size);\n }\n\n @if $enable-smooth-scroll {\n @media (prefers-reduced-motion: no-preference) {\n scroll-behavior: smooth;\n }\n }\n}\n\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Prevent adjustments of font size after orientation changes in iOS.\n// 4. Change the default tap highlight to be completely transparent in iOS.\n\n// scss-docs-start reboot-body-rules\nbody {\n margin: 0; // 1\n font-family: var(--#{$variable-prefix}body-font-family);\n @include font-size(var(--#{$variable-prefix}body-font-size));\n font-weight: var(--#{$variable-prefix}body-font-weight);\n line-height: var(--#{$variable-prefix}body-line-height);\n color: var(--#{$variable-prefix}body-color);\n text-align: var(--#{$variable-prefix}body-text-align);\n background-color: var(--#{$variable-prefix}body-bg); // 2\n -webkit-text-size-adjust: 100%; // 3\n -webkit-tap-highlight-color: rgba($black, 0); // 4\n}\n// scss-docs-end reboot-body-rules\n\n\n// Content grouping\n//\n// 1. Reset Firefox's gray color\n// 2. Set correct height and prevent the `size` attribute to make the `hr` look like an input field\n\nhr {\n margin: $hr-margin-y 0;\n color: $hr-color; // 1\n background-color: currentColor;\n border: 0;\n opacity: $hr-opacity;\n}\n\nhr:not([size]) {\n height: $hr-height; // 2\n}\n\n\n// Typography\n//\n// 1. Remove top margins from headings\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n\n%heading {\n margin-top: 0; // 1\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-style: $headings-font-style;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1 {\n @extend %heading;\n @include font-size($h1-font-size);\n}\n\nh2 {\n @extend %heading;\n @include font-size($h2-font-size);\n}\n\nh3 {\n @extend %heading;\n @include font-size($h3-font-size);\n}\n\nh4 {\n @extend %heading;\n @include font-size($h4-font-size);\n}\n\nh5 {\n @extend %heading;\n @include font-size($h5-font-size);\n}\n\nh6 {\n @extend %heading;\n @include font-size($h6-font-size);\n}\n\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\n\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-bs-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-bs-original-title] { // 1\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n text-decoration-skip-ink: none; // 4\n}\n\n\n// Address\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\n\n// Lists\n\nol,\nul {\n padding-left: 2rem;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\n// 1. Undo browser default\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // 1\n}\n\n\n// Blockquote\n\nblockquote {\n margin: 0 0 1rem;\n}\n\n\n// Strong\n//\n// Add the correct font weight in Chrome, Edge, and Safari\n\nb,\nstrong {\n font-weight: $font-weight-bolder;\n}\n\n\n// Small\n//\n// Add the correct font size in all browsers\n\nsmall {\n @include font-size($small-font-size);\n}\n\n\n// Mark\n\nmark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n// Sub and Sup\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n\nsub,\nsup {\n position: relative;\n @include font-size($sub-sup-font-size);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n// Links\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n\n &:hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([class]) {\n &,\n &:hover {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n// Code\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-code;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n direction: ltr #{\"/* rtl:ignore */\"};\n unicode-bidi: bidi-override;\n}\n\n// 1. Remove browser default top margin\n// 2. Reset browser default of `1em` to use `rem`s\n// 3. Don't allow content to break outside\n\npre {\n display: block;\n margin-top: 0; // 1\n margin-bottom: 1rem; // 2\n overflow: auto; // 3\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n\n kbd {\n padding: 0;\n @include font-size(1em);\n font-weight: $nested-kbd-font-weight;\n }\n}\n\n\n// Figures\n//\n// Apply a consistent margin strategy (matches our type styles).\n\nfigure {\n margin: 0 0 1rem;\n}\n\n\n// Images and content\n\nimg,\nsvg {\n vertical-align: middle;\n}\n\n\n// Tables\n//\n// Prevent double borders\n\ntable {\n caption-side: bottom;\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: $table-cell-padding-y;\n padding-bottom: $table-cell-padding-y;\n color: $table-caption-color;\n text-align: left;\n}\n\n// 1. Removes font-weight bold by inheriting\n// 2. Matches default `` alignment by inheriting `text-align`.\n// 3. Fix alignment for Safari\n\nth {\n font-weight: $table-th-font-weight; // 1\n text-align: inherit; // 2\n text-align: -webkit-match-parent; // 3\n}\n\nthead,\ntbody,\ntfoot,\ntr,\ntd,\nth {\n border-color: inherit;\n border-style: solid;\n border-width: 0;\n}\n\n\n// Forms\n//\n// 1. Allow labels to use `margin` for spacing.\n\nlabel {\n display: inline-block; // 1\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n// See https://github.com/twbs/bootstrap/issues/24093\n\nbutton {\n // stylelint-disable-next-line property-disallowed-list\n border-radius: 0;\n}\n\n// Explicitly remove focus outline in Chromium when it shouldn't be\n// visible (e.g. as result of mouse click or touch tap). It already\n// should be doing this automatically, but seems to currently be\n// confused and applies its very visible two-tone outline anyway.\n\nbutton:focus:not(:focus-visible) {\n outline: 0;\n}\n\n// 1. Remove the margin in Firefox and Safari\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // 1\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\n// Remove the inheritance of text transform in Firefox\nbutton,\nselect {\n text-transform: none;\n}\n// Set the cursor for non-`

/// Block where the code assembly failed. /// Message that describes the failure. -public record HookFailureOnCodeAssembly(HookCodeAssemblySource Source, string Message) - : HookFailure(HookFailureReason.CodeAssemblyFailure) +public record CodeAssemblyFailure(HookCodeAssemblySource Source, string Message) + : Failure($"Failed to assemble code in {GetSourceAsString(Source)}: {Message}") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"Failed to assemble code in {GetSourceAsString(Source)}: {Message}"; + /// Block where the code assembly failed. + public HookCodeAssemblySource Source { get; init; } = Source; /// Returns a string representation of the given . /// The source to convert to a string. /// A string representation of the given . - public static string GetSourceAsString(HookCodeAssemblySource source) => source switch + private static string GetSourceAsString(HookCodeAssemblySource source) => source switch { HookCodeAssemblySource.JumpToInjectedCode => "the jump to the injected code", HookCodeAssemblySource.PrependedCode => "the code block generated before the injected code", @@ -171,15 +38,3 @@ public record HookFailureOnCodeAssembly(HookCodeAssemblySource Source, string Me _ => "an undetermined code block" }; } - -/// -/// Represents a failure that occurred in a hook operation when a write operation failed. -/// -/// Details about the write failure. -public record HookFailureOnWriteFailure(WriteFailure Details) - : HookFailure(HookFailureReason.WriteFailure) -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"A write operation failed: {Details}"; -} diff --git a/src/MindControl/Addressing/IAddressResolver.cs b/src/MindControl/Addressing/IAddressResolver.cs index f914f99..9f32cf4 100644 --- a/src/MindControl/Addressing/IAddressResolver.cs +++ b/src/MindControl/Addressing/IAddressResolver.cs @@ -5,12 +5,12 @@ namespace MindControl; /// /// Provides a way to resolve an address in the target process. /// -public interface IAddressResolver +public interface IAddressResolver { /// /// Resolves the address in the target process using the given instance. /// /// Instance of attached to the target process. /// A result holding either the resolved address, or a failure. - Result ResolveFor(ProcessMemory processMemory); + Result ResolveFor(ProcessMemory processMemory); } \ No newline at end of file diff --git a/src/MindControl/Addressing/LiteralAddressResolver.cs b/src/MindControl/Addressing/LiteralAddressResolver.cs index 5ac744f..b10de5c 100644 --- a/src/MindControl/Addressing/LiteralAddressResolver.cs +++ b/src/MindControl/Addressing/LiteralAddressResolver.cs @@ -7,7 +7,7 @@ namespace MindControl; /// This implementation takes a literal address and always resolves to that same address. ///
/// Literal address to return in . -public class LiteralAddressResolver(UIntPtr address) : IAddressResolver +public class LiteralAddressResolver(UIntPtr address) : IAddressResolver { /// Gets the literal address to return in . public UIntPtr Address => address; @@ -17,10 +17,10 @@ public class LiteralAddressResolver(UIntPtr address) : IAddressResolver ///
/// Instance of attached to the target process. /// A result holding either the resolved address, or a failure expressed as a string. - public Result ResolveFor(ProcessMemory processMemory) + public Result ResolveFor(ProcessMemory processMemory) { if (!processMemory.Is64Bit && Address > uint.MaxValue) - return "Failed to resolve: the address is a 64-bit address, but the target process is 32-bit."; + return new IncompatibleBitnessPointerFailure(Address); return Address; } } \ No newline at end of file diff --git a/src/MindControl/Addressing/MemoryRange.cs b/src/MindControl/Addressing/MemoryRange.cs index 48cddb4..81a26a0 100644 --- a/src/MindControl/Addressing/MemoryRange.cs +++ b/src/MindControl/Addressing/MemoryRange.cs @@ -113,11 +113,11 @@ public IEnumerable Exclude(MemoryRange rangeToExclude) { // This range is entirely contained in the range to subtract: no range left if (rangeToExclude.Contains(this)) - return Array.Empty(); + return []; // No overlap between the two ranges: the original range is returned, untouched if (!rangeToExclude.Overlaps(this)) - return new[] { this }; + return [this]; // There is an overlap: either one or two ranges will be returned, depending on the overlap var results = new List(); diff --git a/src/MindControl/Addressing/PointerPath.cs b/src/MindControl/Addressing/PointerPath.cs index 3f61114..1563a65 100644 --- a/src/MindControl/Addressing/PointerPath.cs +++ b/src/MindControl/Addressing/PointerPath.cs @@ -9,9 +9,9 @@ public class PointerPath { /// - /// Stores data parsed and computed internally from a pointer path expression. + /// Stores the properties of a pointer path expression that are used in computations. /// - private readonly struct ExpressionParsedData + private readonly struct ExpressionInternalData { /// /// Gets the base module name. @@ -39,34 +39,36 @@ private readonly struct ExpressionParsedData /// public bool IsStrictly64Bit => BaseModuleOffset.Is64Bit || PointerOffsets.Any(offset => offset.Is64Bit); } + + private string? _expression; /// /// Gets the pointer path expression. Some examples include "myprocess.exe+001F468B,1B,0,A0", /// or "1F07A314", or "1F07A314+A0", or "1F07A314-A0,4". /// - public string Expression { get; } + public string Expression => _expression ??= BuildExpression(); - private readonly ExpressionParsedData _parsedData; + private readonly ExpressionInternalData _internalData; /// /// Gets the base module name. /// For example, for the expression "myprocess.exe+01F4684-4,18+4,C", gets "myprocess.exe". /// For expression with no module name, like "01F4684-4,18", gets a null value. /// - public string? BaseModuleName => _parsedData.BaseModuleName; + public string? BaseModuleName => _internalData.BaseModuleName; /// /// Gets the offset of the base module name. /// For example, for the expression "myprocess.exe+01F4684-4,18+4,C", gets 0x1F4680. /// For expressions without a module offset, like "01F4684-4,18" or "myprocess.exe", gets 0. /// - public PointerOffset BaseModuleOffset => _parsedData.BaseModuleOffset; + public PointerOffset BaseModuleOffset => _internalData.BaseModuleOffset; /// /// Gets the collection of pointer offsets to follow sequentially in order to evaluate the memory address. /// For example, for the expression "myprocess.exe+01F4684-4,18+4,C", gets [0x1C, 0xC]. /// - public PointerOffset[] PointerOffsets => _parsedData.PointerOffsets; + public PointerOffset[] PointerOffsets => _internalData.PointerOffsets; /// /// Gets a boolean indicating if the path is a 64-bit only path, or if it can also be used in a 32-bit @@ -75,7 +77,7 @@ private readonly struct ExpressionParsedData /// For the expression "app.dll+00000000F04AA121", this boolean would be False. /// Note that evaluating a 32-bit-compatible address may still end up overflowing. /// - public bool IsStrictly64Bit => _parsedData.IsStrictly64Bit; + public bool IsStrictly64Bit => _internalData.IsStrictly64Bit; /// /// Builds a pointer path from the given expression. @@ -84,7 +86,7 @@ private readonly struct ExpressionParsedData /// or "1F07A314", or "1F07A314+A0", or "1F07A314-A0,4". /// Thrown when the expression is not valid. public PointerPath(string expression) : this(expression, Parse(expression)) {} - + /// /// Builds a pointer path from the given expression and parsed data. /// @@ -92,14 +94,51 @@ public PointerPath(string expression) : this(expression, Parse(expression)) {} /// or "1F07A314", or "1F07A314+A0", or "1F07A314-A0,4". /// An instance containing data that was already parsed from the expression. /// Thrown when the expression is not valid. - private PointerPath(string expression, ExpressionParsedData? parsedData) + private PointerPath(string expression, ExpressionInternalData? parsedData) { - Expression = expression; - _parsedData = parsedData ?? throw new ArgumentException( + _expression = expression; + _internalData = parsedData ?? throw new ArgumentException( $"The provided expression \"{expression}\" is not valid. Please check the expression syntax guide for more information.", nameof(expression)); } + /// + /// Builds a pointer path from a pointer and a series of offsets. + /// + /// Address of the base pointer, which is the first address to evaluate. + /// For example, in "1F07A314,4,1C", this would be 0x1F07A314. + /// Collection of offsets to follow sequentially in order to evaluate the + /// final memory address. For example, in "1F07A314,4,1C", this would be [0x4, 0x1C]. + public PointerPath(UIntPtr basePointerAddress, params long[] pointerOffsets) + { + _internalData = new ExpressionInternalData + { + BaseModuleName = null, + BaseModuleOffset = PointerOffset.Zero, + PointerOffsets = new[] { new PointerOffset(basePointerAddress.ToUInt64(), false) } + .Concat(pointerOffsets.Select(o => new PointerOffset((ulong)Math.Abs(o), o < 0))).ToArray() + }; + } + + /// + /// Builds a pointer path from a base module name, a base module offset, and a series of offsets. + /// + /// Name of the base module, where the starting pointer is found. For example, in + /// "mygame.exe+3FF0,4,1C", this would be "mygame.exe" (without the quotes). + /// Offset applied to the base module to get the address of the first pointer to + /// evaluate. For example, in "mygame.exe+3FF0,4,1C", this would be 0x3FF0. + /// Collection of offsets to follow sequentially in order to evaluate the + /// final memory address. For example, in "mygame.exe+3FF0,4,1C", this would be [0x4, 0x1C]. + public PointerPath(string baseModuleName, UIntPtr baseModuleOffset, params long[] pointerOffsets) + { + _internalData = new ExpressionInternalData + { + BaseModuleName = baseModuleName, + BaseModuleOffset = new PointerOffset(baseModuleOffset.ToUInt64(), false), + PointerOffsets = pointerOffsets.Select(o => new PointerOffset((ulong)Math.Abs(o), o < 0)).ToArray() + }; + } + /// /// Implicitly converts the given string to a instance using the constructor. /// @@ -142,7 +181,7 @@ public static bool IsValid(string expression, bool allowOnly32Bit = false) /// /// Expression to parse. /// the parsed data container when successful, or null if the expression is not valid. - private static ExpressionParsedData? Parse(string expression) + private static ExpressionInternalData? Parse(string expression) { // A note about the parsing code: // It is designed to be fast and to allocate the strict minimum amount of memory. @@ -166,11 +205,11 @@ public static bool IsValid(string expression, bool allowOnly32Bit = false) // If the whole expression was the module name, we can return the parsed data if (hasModule && readCount == expression.Length) { - return new ExpressionParsedData + return new ExpressionInternalData { BaseModuleName = baseModuleName, BaseModuleOffset = PointerOffset.Zero, - PointerOffsets = Array.Empty() + PointerOffsets = [] }; } @@ -212,7 +251,7 @@ public static bool IsValid(string expression, bool allowOnly32Bit = false) index += readCount; } - return new ExpressionParsedData + return new ExpressionInternalData { BaseModuleName = hasModule ? baseModuleName : null, BaseModuleOffset = baseModuleOffset ?? PointerOffset.Zero, @@ -258,7 +297,7 @@ public static bool IsValid(string expression, bool allowOnly32Bit = false) // The module name is not quoted. If there is a module name, it will end either with a +/- operator, a ',', or // the end of the string. - int endIndex = expression.IndexOfAny(new[] { '-', '+', ',' }); + int endIndex = expression.IndexOfAny(['-', '+', ',']); if (endIndex == -1) endIndex = expression.Length; @@ -387,6 +426,14 @@ private static byte CharToValue(char c) _ => 255 }; } + + /// Builds the expression string from the properties. Used when the pointer path is built from pointers and + /// not parsed from a string expression. + private string BuildExpression() + { + var offsetString = string.Join(',', PointerOffsets.Select(o => o.ToString())); + return BaseModuleName != null ? $"\"{BaseModuleName}\"+{BaseModuleOffset},{offsetString}" : offsetString; + } /// Returns a string that represents the current object. /// A string that represents the current object. diff --git a/src/MindControl/Addressing/PointerPathResolver.cs b/src/MindControl/Addressing/PointerPathResolver.cs index 1d77df0..ef7e791 100644 --- a/src/MindControl/Addressing/PointerPathResolver.cs +++ b/src/MindControl/Addressing/PointerPathResolver.cs @@ -6,8 +6,8 @@ namespace MindControl; /// Provides a way to resolve an address in the target process. /// This implementation takes a pointer path and resolves it to an address in the target process. ///
-/// -public class PointerPathResolver(PointerPath pointerPath) : IAddressResolver +/// Target pointer path. +public class PointerPathResolver(PointerPath pointerPath) : IAddressResolver { /// Gets the pointer path to resolve. public PointerPath PointerPath { get; } = pointerPath; @@ -17,6 +17,5 @@ public class PointerPathResolver(PointerPath pointerPath) : IAddressResolver /// Instance of attached to the target process. /// A result holding either the resolved address, or a failure. - public Result ResolveFor(ProcessMemory processMemory) => - processMemory.EvaluateMemoryAddress(PointerPath); + public Result ResolveFor(ProcessMemory processMemory) => processMemory.EvaluateMemoryAddress(PointerPath); } \ No newline at end of file diff --git a/src/MindControl/Addressing/ProcessMemoryStream.cs b/src/MindControl/Addressing/ProcessMemoryStream.cs index ac5e01f..9c2e798 100644 --- a/src/MindControl/Addressing/ProcessMemoryStream.cs +++ b/src/MindControl/Addressing/ProcessMemoryStream.cs @@ -31,8 +31,8 @@ internal ProcessMemoryStream(IOperatingSystemService osService, IntPtr processHa public override bool CanRead => true; /// Returns False to indicate that this stream does not support seeking. - /// . - public override bool CanSeek => false; + /// . + public override bool CanSeek => true; /// Returns True to indicate that this stream supports writing. /// diff --git a/src/MindControl/Allocation/MemoryAllocation.cs b/src/MindControl/Allocation/MemoryAllocation.cs index 9b13684..e8fe11a 100644 --- a/src/MindControl/Allocation/MemoryAllocation.cs +++ b/src/MindControl/Allocation/MemoryAllocation.cs @@ -149,16 +149,16 @@ private IEnumerable GetFreeRanges() /// only accomodate 32 bytes. Alignment means the actual reserved space might be bigger than the /// , but will never make it smaller. /// A result holding the resulting reservation or a reservation failure. - public Result ReserveRange(ulong size, uint? byteAlignment = 8) + public Result ReserveRange(ulong size, uint? byteAlignment = 8) { if (IsDisposed || !_parentProcessMemory.IsAttached) - return new ReservationFailureOnDisposedAllocation(); + return new DisposedAllocationFailure(); if (size == 0) - return new ReservationFailureOnInvalidArguments("The size to reserve must be more than 0 bytes."); + return new InvalidArgumentFailure(nameof(size), "The size to reserve must be more than 0 bytes."); var range = GetNextRangeFittingSize(size, byteAlignment); if (range == null) - return new ReservationFailureOnNoSpaceAvailable(); + return new InsufficientSpaceFailure(); var reservedRange = new MemoryReservation(range.Value, this); _reservations.Add(reservedRange); diff --git a/src/MindControl/Anchors/ByteArrayMemoryAdapter.cs b/src/MindControl/Anchors/ByteArrayMemoryAdapter.cs index e048b3b..68f68b1 100644 --- a/src/MindControl/Anchors/ByteArrayMemoryAdapter.cs +++ b/src/MindControl/Anchors/ByteArrayMemoryAdapter.cs @@ -6,10 +6,9 @@ namespace MindControl.Anchors; /// Represents an adapter for reading and writing a value from and to memory. /// This implementation reads and writes a byte array from and to memory using an address resolver and a size. ///
-/// -public class ByteArrayMemoryAdapter : IMemoryAdapter +public class ByteArrayMemoryAdapter : IMemoryAdapter { - private readonly IAddressResolver _addressResolver; + private readonly IAddressResolver _addressResolver; private readonly int _size; /// @@ -18,8 +17,7 @@ public class ByteArrayMemoryAdapter : IMemoryAdapter /// Resolver that provides the address of the array in memory. /// Size of the byte array to read and write. - /// - public ByteArrayMemoryAdapter(IAddressResolver addressResolver, int size) + public ByteArrayMemoryAdapter(IAddressResolver addressResolver, int size) { if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size), "The size must be greater than zero."); @@ -31,11 +29,11 @@ public ByteArrayMemoryAdapter(IAddressResolver addressResolver, /// Reads the value in the memory of the target process. /// Instance of attached to the target process. /// A result holding either the value read from memory, or a failure. - public Result Read(ProcessMemory processMemory) + public Result Read(ProcessMemory processMemory) { var addressResult = _addressResolver.ResolveFor(processMemory); if (addressResult.IsFailure) - return new ReadFailureOnAddressResolution(addressResult.Error); + return addressResult.Failure; return processMemory.ReadBytes(addressResult.Value, _size); } @@ -44,15 +42,15 @@ public Result Read(ProcessMemory processMemory) /// Instance of attached to the target process. /// Value to write to memory. /// A result indicating success or failure. - public Result Write(ProcessMemory processMemory, byte[] value) + public Result Write(ProcessMemory processMemory, byte[] value) { if (value.Length != _size) - return new WriteFailureOnInvalidArguments( + return new InvalidArgumentFailure(nameof(value), $"The size of the byte array does not match the expected size of {_size}."); var addressResult = _addressResolver.ResolveFor(processMemory); if (addressResult.IsFailure) - return new WriteFailureOnAddressResolution(addressResult.Error); + return addressResult.Failure; return processMemory.WriteBytes(addressResult.Value, value); } diff --git a/src/MindControl/Anchors/GenericMemoryAdapter.cs b/src/MindControl/Anchors/GenericMemoryAdapter.cs index 4aa9d88..6adbc48 100644 --- a/src/MindControl/Anchors/GenericMemoryAdapter.cs +++ b/src/MindControl/Anchors/GenericMemoryAdapter.cs @@ -8,19 +8,17 @@ namespace MindControl.Anchors; /// /// Resolver that provides the address of the value in memory. /// Type of the value to read and write. -/// Type of the failure that can occur when resolving the address of the value. -/// -public class GenericMemoryAdapter(IAddressResolver addressResolver) - : IMemoryAdapter where TValue : struct +public class GenericMemoryAdapter(IAddressResolver addressResolver) + : IMemoryAdapter where TValue : struct { /// Reads the value in the memory of the target process. /// Instance of attached to the target process. /// A result holding either the value read from memory, or a failure. - public Result Read(ProcessMemory processMemory) + public Result Read(ProcessMemory processMemory) { var addressResult = addressResolver.ResolveFor(processMemory); if (addressResult.IsFailure) - return new ReadFailureOnAddressResolution(addressResult.Error); + return addressResult.Failure; return processMemory.Read(addressResult.Value); } @@ -29,11 +27,11 @@ public Result Read(ProcessMemory processMemory) /// Instance of attached to the target process. /// Value to write to memory. /// A result indicating success or failure. - public Result Write(ProcessMemory processMemory, TValue value) + public Result Write(ProcessMemory processMemory, TValue value) { var addressResult = addressResolver.ResolveFor(processMemory); if (addressResult.IsFailure) - return new WriteFailureOnAddressResolution(addressResult.Error); + return addressResult.Failure; return processMemory.Write(addressResult.Value, value, MemoryProtectionStrategy.Ignore); } diff --git a/src/MindControl/Anchors/IMemoryAdapter.cs b/src/MindControl/Anchors/IMemoryAdapter.cs index 6965c4a..08aa05f 100644 --- a/src/MindControl/Anchors/IMemoryAdapter.cs +++ b/src/MindControl/Anchors/IMemoryAdapter.cs @@ -6,18 +6,16 @@ namespace MindControl.Anchors; /// Represents an adapter for reading and writing a value from and to memory. ///
/// Type of the value to read and write. -/// Type of the failure that can occur when reading the value. -/// Type of the failure that can occur when writing the value. -public interface IMemoryAdapter +public interface IMemoryAdapter { /// Reads the value in the memory of the target process. /// Instance of attached to the target process. /// A result holding either the value read from memory, or a failure. - Result Read(ProcessMemory processMemory); + Result Read(ProcessMemory processMemory); /// Writes the value to the memory of the target process. /// Instance of attached to the target process. /// Value to write to memory. /// A result indicating success or failure. - Result Write(ProcessMemory processMemory, TValue value); + Result Write(ProcessMemory processMemory, TValue value); } \ No newline at end of file diff --git a/src/MindControl/Anchors/StringPointerMemoryAdapter.cs b/src/MindControl/Anchors/StringPointerMemoryAdapter.cs index de2f06f..b955d18 100644 --- a/src/MindControl/Anchors/StringPointerMemoryAdapter.cs +++ b/src/MindControl/Anchors/StringPointerMemoryAdapter.cs @@ -8,19 +8,17 @@ namespace MindControl.Anchors; ///
/// Resolver that provides the address of the string in memory. /// Settings that define how the string is read and written. -/// Type of the failure that can occur when resolving the address of the string. -/// -public class StringPointerMemoryAdapter(IAddressResolver addressResolver, - StringSettings stringSettings) : IMemoryAdapter +public class StringPointerMemoryAdapter(IAddressResolver addressResolver, + StringSettings stringSettings) : IMemoryAdapter { /// Reads the value in the memory of the target process. /// Instance of attached to the target process. /// A result holding either the value read from memory, or a failure. - public Result Read(ProcessMemory processMemory) + public Result Read(ProcessMemory processMemory) { var addressResult = addressResolver.ResolveFor(processMemory); if (addressResult.IsFailure) - return new StringReadFailureOnAddressResolution(addressResult.Error); + return addressResult.Failure; return processMemory.ReadStringPointer(addressResult.Value, stringSettings); } @@ -29,10 +27,9 @@ public Result Read(ProcessMemory processMemory) /// Instance of attached to the target process. /// Value to write to memory. /// A result indicating success or failure. - public Result Write(ProcessMemory processMemory, string value) + public Result Write(ProcessMemory processMemory, string value) { // Not supported for now, might be in the future if we implement internal string instance management. - return new NotSupportedFailure( - "Writing a string to memory is not supported in this context, because it involves memory allocations that must be handled separately."); + return new NotSupportedFailure("Writing a string to memory is not supported in this context, because it involves memory allocations that must be handled separately. As an alternative, use StoreString, and write a pointer to the returned address."); } } \ No newline at end of file diff --git a/src/MindControl/Anchors/ThreadValueFreezer.cs b/src/MindControl/Anchors/ThreadValueFreezer.cs index c90efc1..7075afa 100644 --- a/src/MindControl/Anchors/ThreadValueFreezer.cs +++ b/src/MindControl/Anchors/ThreadValueFreezer.cs @@ -2,23 +2,21 @@ /// Provides methods to freeze a value in memory, using a thread that constantly writes the value. /// Type of the value to freeze. -/// Type of the failure that can occur when reading the value. -/// Type of the failure that can occur when writing the value. -public class ThreadValueFreezer : IDisposable +public class ThreadValueFreezer : IDisposable { - private readonly ValueAnchor _anchor; + private readonly ValueAnchor _anchor; private readonly TValue _value; private bool _disposed; /// Event raised when a freeze operation fails. - public event EventHandler>? FreezeFailed; + public event EventHandler? FreezeFailed; /// /// Freezes a value in memory, using a thread that constantly writes the target value. /// /// Anchor holding the memory value to freeze. /// Value to freeze in memory. - public ThreadValueFreezer(ValueAnchor anchor, TValue value) + public ThreadValueFreezer(ValueAnchor anchor, TValue value) { _anchor = anchor; _value = value; @@ -35,7 +33,7 @@ private void WriteForever() { var result = _anchor.Write(_value); if (result.IsFailure) - FreezeFailed?.Invoke(this, new FreezeFailureEventArgs(result.Error)); + FreezeFailed?.Invoke(this, new FreezeFailureEventArgs(result.Failure)); } } diff --git a/src/MindControl/Anchors/TimerValueFreezer.cs b/src/MindControl/Anchors/TimerValueFreezer.cs index 215c0ee..c63c9eb 100644 --- a/src/MindControl/Anchors/TimerValueFreezer.cs +++ b/src/MindControl/Anchors/TimerValueFreezer.cs @@ -1,25 +1,29 @@ -using MindControl.State; +using MindControl.Results; +using MindControl.State; namespace MindControl.Anchors; /// Event arguments used when a freeze operation fails. -/// Failure that occurred when trying to freeze the value. -/// Type of the failure. -public class FreezeFailureEventArgs(TFailure Failure) : EventArgs; +/// Failure that occurred when trying to freeze the value. +public class FreezeFailureEventArgs(Failure failure) : EventArgs +{ + /// Gets the failure that occurred when trying to freeze the value. + public Failure Failure { get; } = failure; +} /// /// Provides methods to freeze a value in memory, using a timer to write the value at regular intervals. /// -public class TimerValueFreezer : IDisposable +public class TimerValueFreezer : IDisposable { private readonly PrecisionTimer _timer; - private readonly ValueAnchor _anchor; + private readonly ValueAnchor _anchor; private readonly TValue _value; private bool _isTicking; private bool _disposed; /// Event raised when a freeze operation fails. - public event EventHandler>? FreezeFailed; + public event EventHandler? FreezeFailed; /// /// Freezes a value in memory, using a timer to write the value at regular intervals. @@ -27,8 +31,7 @@ public class TimerValueFreezer : IDisposabl /// Anchor holding the memory value to freeze. /// Value to freeze in memory. /// Interval at which the value should be written to memory. - public TimerValueFreezer(ValueAnchor anchor, TValue value, - TimeSpan timerInterval) + public TimerValueFreezer(ValueAnchor anchor, TValue value, TimeSpan timerInterval) { _anchor = anchor; _value = value; @@ -51,7 +54,7 @@ private void OnTimerTick(object? sender, EventArgs e) { var result = _anchor.Write(_value); if (result.IsFailure) - FreezeFailed?.Invoke(this, new FreezeFailureEventArgs(result.Error)); + FreezeFailed?.Invoke(this, new FreezeFailureEventArgs(result.Failure)); } finally { diff --git a/src/MindControl/Anchors/ValueAnchor.cs b/src/MindControl/Anchors/ValueAnchor.cs index bfaa3b6..eedb91b 100644 --- a/src/MindControl/Anchors/ValueAnchor.cs +++ b/src/MindControl/Anchors/ValueAnchor.cs @@ -6,61 +6,43 @@ namespace MindControl.Anchors; /// Adapter that reads and writes the value from and to memory. /// Instance of attached to the target process. /// Type of the value to read and write. -/// Type of the failure that can occur when reading the value. -/// Type of the failure that can occur when writing the value. -public class ValueAnchor - (IMemoryAdapter memoryAdapter, ProcessMemory processMemory) +public class ValueAnchor(IMemoryAdapter memoryAdapter, ProcessMemory processMemory) { /// Reads the value in the memory of the target process. /// A result holding either the value read from memory, or a failure. - public Result Read() - { - var result = memoryAdapter.Read(processMemory); - if (result.IsFailure) - return result.Error; - return result.Value; - } + public Result Read() => memoryAdapter.Read(processMemory); /// Writes the value to the memory of the target process. /// Value to write to memory. /// A result indicating success or failure. - public Result Write(TValue value) - { - var result = memoryAdapter.Write(processMemory, value); - if (result.IsFailure) - return result.Error; - return Result.Success; - } + public Result Write(TValue value) => memoryAdapter.Write(processMemory, value); /// Freezes the memory area that the anchor is attached to, constantly overwriting its value, until the /// resulting instance is disposed. - /// A instance that can be disposed to + /// A instance that can be disposed to /// stop freezing the value, and provides a subscribable event raised when a recurrent write operation fails. /// - public TimerValueFreezer Freeze(TValue value) + public TimerValueFreezer Freeze(TValue value) { // Use an arbitrary 150 updates per second as the default interval (a bit more than the standard 144FPS). - return new TimerValueFreezer(this, value, - TimeSpan.FromSeconds(1 / 150f)); + return new TimerValueFreezer(this, value, TimeSpan.FromSeconds(1 / 150f)); } /// - /// Provides a instance that periodically reads the + /// Provides a instance that periodically reads the /// value from the anchor and raises events when the value changes, until it is disposed. /// /// Target time interval between each read operation in the watcher. - /// A instance that periodically reads the + /// A instance that periodically reads the /// value from the anchor and raises events when the value changes, until it is disposed. - public ValueWatcher Watch(TimeSpan refreshInterval) - => new(this, refreshInterval); + public ValueWatcher Watch(TimeSpan refreshInterval) => new(this, refreshInterval); /// - /// Provides a instance that periodically reads the + /// Provides a instance that periodically reads the /// value from the anchor and raises events when the value changes, until it is disposed. /// /// Target number of reads per second of the watcher. - /// A instance that periodically reads the + /// A instance that periodically reads the /// value from the anchor and raises events when the value changes, until it is disposed. - public ValueWatcher Watch(int updatesPerSecond) - => new(this, TimeSpan.FromSeconds(1f / updatesPerSecond)); + public ValueWatcher Watch(int updatesPerSecond) => new(this, TimeSpan.FromSeconds(1f / updatesPerSecond)); } \ No newline at end of file diff --git a/src/MindControl/Anchors/ValueWatcher.cs b/src/MindControl/Anchors/ValueWatcher.cs index f34ebba..196f5ec 100644 --- a/src/MindControl/Anchors/ValueWatcher.cs +++ b/src/MindControl/Anchors/ValueWatcher.cs @@ -3,7 +3,7 @@ namespace MindControl.Anchors; /// -/// Event arguments used when a value observed by a +/// Event arguments used when a value observed by a /// changes. /// /// Last known value before the change. @@ -19,7 +19,7 @@ public class ValueChangedEventArgs(TValue? previousValue, TValue newValu } /// -/// Event arguments used when a value observed by a +/// Event arguments used when a value observed by a /// becomes unreadable (causes a read failure). This may happen when the target process frees or rearranges its memory, /// or when the related instance is detached. /// @@ -31,7 +31,7 @@ public class ValueLostEventArgs(TValue lastKnownValue) : EventArgs } /// -/// Event arguments used when a value observed by a +/// Event arguments used when a value observed by a /// is successfully read after being lost. /// /// New value freshly read. @@ -46,11 +46,9 @@ public class ValueReacquiredEventArgs(TValue newValue) : EventArgs /// Uses a timer to periodically read a value from a given anchor and raise events when the value changes. /// /// Type of the value held by the anchor. -/// Type of the failure that can occur when reading the value. -/// Type of the failure that can occur when writing the value. -public class ValueWatcher : IDisposable +public class ValueWatcher : IDisposable { - private readonly ValueAnchor _anchor; + private readonly ValueAnchor _anchor; private readonly PrecisionTimer _timer; private bool _isDisposed; private readonly SemaphoreSlim _updateLock = new(1, 1); @@ -88,10 +86,7 @@ public class ValueWatcher : IDisposable ///
/// The anchor holding the value to watch. /// Target time interval between each read operation. - /// Type of the value held by the anchor. - /// Type of the failure that can occur when reading the value. - /// Type of the failure that can occur when writing the value. - public ValueWatcher(ValueAnchor anchor, TimeSpan refreshInterval) + public ValueWatcher(ValueAnchor anchor, TimeSpan refreshInterval) { _anchor = anchor; LastChangeTime = DateTime.Now; diff --git a/src/MindControl/MindControl.csproj b/src/MindControl/MindControl.csproj index d6f37ee..6c91d69 100644 --- a/src/MindControl/MindControl.csproj +++ b/src/MindControl/MindControl.csproj @@ -7,7 +7,7 @@ MindControl https://github.com/Doublevil/mind-control git - 0.5.1 + 1.0.0-alpha-25032201 diff --git a/src/MindControl/Modules/PeParser.cs b/src/MindControl/Modules/PeParser.cs index c086309..7d555a1 100644 --- a/src/MindControl/Modules/PeParser.cs +++ b/src/MindControl/Modules/PeParser.cs @@ -17,21 +17,21 @@ internal class PeParser(ProcessMemory processMemory, UIntPtr imageBaseAddress) /// Reads and parses the export table of the module, associating the names of the exported functions with their /// absolute addresses in the process memory. ///
- public Result, string> ReadExportTable() + public Result> ReadExportTable() { var exportTable = new Dictionary(); // Read the PE header address var peHeaderRva = processMemory.Read(imageBaseAddress + PeHeaderAddressOffset); if (peHeaderRva.IsFailure) - return "Could not read the PE header address from the DOS header."; + return new Failure("Could not read the PE header address from the DOS header."); var peHeaderAddress = imageBaseAddress + peHeaderRva.Value; // Read the magic number from the Optional Header var optionalHeaderAddress = peHeaderAddress + 24; // Skip over the 20-byte File Header and 4-byte PE signature var magicNumber = processMemory.Read(optionalHeaderAddress); if (magicNumber.IsFailure) - return "Could not read the magic number."; + return new Failure("Could not read the magic number."); bool? is64Bit = magicNumber.Value switch { @@ -41,39 +41,39 @@ public Result, string> ReadExportTable() }; if (is64Bit == null) - return $"Invalid magic number value: 0x{magicNumber.Value:X}."; + return new Failure($"Invalid magic number value: 0x{magicNumber.Value:X}."); // Read the export table address UIntPtr exportTableAddressPointer = peHeaderAddress + (UIntPtr)(is64Bit.Value ? 0x88 : 0x78); var exportTableAddressRva = processMemory.Read(exportTableAddressPointer); if (exportTableAddressRva.IsFailure) - return "Could not read the export table address."; + return new Failure("Could not read the export table address."); // Read the export table size var exportTableSize = processMemory.Read(exportTableAddressPointer + 4); if (exportTableSize.IsFailure) - return "Could not read the export table size."; + return new Failure("Could not read the export table size."); // Read the number of exported functions var exportTableAddress = imageBaseAddress + exportTableAddressRva.Value; var numberOfFunctions = processMemory.Read(exportTableAddress + 24); if (numberOfFunctions.IsFailure) - return "Could not read the number of exported functions."; + return new Failure("Could not read the number of exported functions."); // Read the export name pointers table (ENPT) var enptBytes = ReadExportTableBytes(exportTableAddress + 32, numberOfFunctions.Value * 4); if (enptBytes == null) - return "Could not read the export name pointers table."; + return new Failure("Could not read the export name pointers table."); // Read the export ordinal table (EOT) var eotBytes = ReadExportTableBytes(exportTableAddress + 36, numberOfFunctions.Value * 2); if (eotBytes == null) - return "Could not read the export ordinal table."; + return new Failure("Could not read the export ordinal table."); // Read the export address table (EAT) var eatBytes = ReadExportTableBytes(exportTableAddress + 28, numberOfFunctions.Value * 4); if (eatBytes == null) - return "Could not read the export address table."; + return new Failure("Could not read the export address table."); for (int i = 0; i < numberOfFunctions.Value; i++) { diff --git a/src/MindControl/Modules/RemoteModule.cs b/src/MindControl/Modules/RemoteModule.cs index bb75c29..992d51c 100644 --- a/src/MindControl/Modules/RemoteModule.cs +++ b/src/MindControl/Modules/RemoteModule.cs @@ -43,7 +43,7 @@ public MemoryRange GetRange() ///
/// A result holding either a dictionary containing the names and addresses of the exported functions, or /// an error message in case the export table could not be read. - public Result, string> ReadExportTable() + public Result> ReadExportTable() { var peParser = new PeParser(_processMemory, (UIntPtr)_internalModule.BaseAddress); return peParser.ReadExportTable(); diff --git a/src/MindControl/Native/IOperatingSystemService.cs b/src/MindControl/Native/IOperatingSystemService.cs index 3a38539..2b81089 100644 --- a/src/MindControl/Native/IOperatingSystemService.cs +++ b/src/MindControl/Native/IOperatingSystemService.cs @@ -12,14 +12,14 @@ public interface IOperatingSystemService ///
/// Identifier of the target process. /// A result holding either the handle of the opened process, or a system failure. - Result OpenProcess(int pid); + Result OpenProcess(int pid); /// /// Returns a value indicating if the process with the given identifier is a 64-bit process or not. /// /// Identifier of the target process. /// A result holding either a boolean indicating if the process is 64-bit, or a system failure. - Result IsProcess64Bit(int pid); + Result IsProcess64Bit(int pid); /// /// Reads a targeted range of the memory of a specified process. @@ -29,7 +29,7 @@ public interface IOperatingSystemService /// Length of the memory range to read. /// A result holding either an array of bytes containing the data read from the process memory, or a /// system failure. - Result ReadProcessMemory(IntPtr processHandle, UIntPtr baseAddress, ulong length); + Result ReadProcessMemory(IntPtr processHandle, UIntPtr baseAddress, ulong length); /// /// Reads a targeted range of the memory of a specified process into the given buffer. Supports partial reads, in @@ -44,7 +44,7 @@ public interface IOperatingSystemService /// Length of the memory range to read. /// A result holding either the number of bytes actually read from memory, or a system failure when no byte /// were successfully read. - Result ReadProcessMemoryPartial(IntPtr processHandle, UIntPtr baseAddress, byte[] buffer, + Result ReadProcessMemoryPartial(IntPtr processHandle, UIntPtr baseAddress, byte[] buffer, int offset, ulong length); /// @@ -60,7 +60,7 @@ Result ReadProcessMemoryPartial(IntPtr processHandle, UInt /// changed, or a system failure. /// The process handle is invalid (zero pointer). /// The target address is invalid (zero pointer). - Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bit, + Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bit, UIntPtr targetAddress, MemoryProtection newProtection); /// @@ -71,7 +71,7 @@ Result ReadAndOverwriteProtection(IntPtr proces /// Base address in the memory of the process to which data will be written. /// Bytes to write in the process memory. /// A result indicating either a success or a system failure. - Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, Span value); + Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, Span value); /// /// Allocates memory in the specified process. The address is determined automatically by the operating system. @@ -81,7 +81,7 @@ Result ReadAndOverwriteProtection(IntPtr proces /// Type of memory allocation. /// Protection flags of the memory to allocate. /// A result holding either a pointer to the start of the allocated memory, or a system failure. - Result AllocateMemory(IntPtr processHandle, int size, MemoryAllocationType allocationType, + Result AllocateMemory(IntPtr processHandle, int size, MemoryAllocationType allocationType, MemoryProtection protection); /// @@ -93,7 +93,7 @@ Result AllocateMemory(IntPtr processHandle, int size, Me /// Type of memory allocation. /// Protection flags of the memory to allocate. /// A result holding either a pointer to the start of the allocated memory, or a system failure. - Result AllocateMemory(IntPtr processHandle, UIntPtr address, int size, + Result AllocateMemory(IntPtr processHandle, UIntPtr address, int size, MemoryAllocationType allocationType, MemoryProtection protection); /// @@ -103,7 +103,7 @@ Result AllocateMemory(IntPtr processHandle, UIntPtr addr /// Address of the start routine to be executed by the thread. /// Address of any parameter to be passed to the start routine. /// A result holding either the handle of the thread, or a system failure. - Result CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, + Result CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, UIntPtr parameterAddress); /// @@ -113,7 +113,7 @@ Result CreateRemoteThread(IntPtr processHandle, UIntPtr s /// Maximum time to wait for the thread to finish. /// A result holding either the exit code of the thread, or a thread failure when the operation failed. /// - Result WaitThread(IntPtr threadHandle, TimeSpan timeout); + Result WaitThread(IntPtr threadHandle, TimeSpan timeout); /// /// Frees the memory allocated in the specified process for a region or a placeholder. @@ -122,14 +122,14 @@ Result CreateRemoteThread(IntPtr processHandle, UIntPtr s /// Base address of the region or placeholder to free, as returned by the allocation /// methods. /// A result indicating either a success or a system failure. - Result ReleaseMemory(IntPtr processHandle, UIntPtr regionBaseAddress); + Result ReleaseMemory(IntPtr processHandle, UIntPtr regionBaseAddress); /// /// Closes the given handle. /// /// Handle to close. /// A result indicating either a success or a system failure. - Result CloseHandle(IntPtr handle); + Result CloseHandle(IntPtr handle); /// /// Gets the range of memory addressable by applications in the current system. @@ -142,7 +142,7 @@ Result CreateRemoteThread(IntPtr processHandle, UIntPtr s /// Handle of the target process. /// Base address of the target memory region. /// A result holding either the metadata of the target memory region, or a system failure. - Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress); + Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress); /// /// Gets the page size of the system. diff --git a/src/MindControl/Native/Win32Service.cs b/src/MindControl/Native/Win32Service.cs index 924bbdc..65d8e45 100644 --- a/src/MindControl/Native/Win32Service.cs +++ b/src/MindControl/Native/Win32Service.cs @@ -15,10 +15,13 @@ public partial class Win32Service : IOperatingSystemService /// /// Builds and returns a failure object representing the last Win32 error that occurred. /// - private static OperatingSystemCallFailure GetLastSystemError() + /// Name of the API function that failed. + /// Name of the top-level operation that failed. + private static OperatingSystemCallFailure GetLastSystemErrorAsFailure(string apiFunctionName, string operationName) { int errorCode = Marshal.GetLastWin32Error(); - return new OperatingSystemCallFailure(errorCode, new Win32Exception(errorCode).Message); + return new OperatingSystemCallFailure(apiFunctionName, operationName, errorCode, + new Win32Exception(errorCode).Message); } /// @@ -26,11 +29,11 @@ private static OperatingSystemCallFailure GetLastSystemError() /// /// Identifier of the target process. /// A result holding either the handle of the opened process, or a system failure. - public Result OpenProcess(int pid) + public Result OpenProcess(int pid) { var handle = OpenProcess(0x1F0FFF, true, pid); if (handle == IntPtr.Zero) - return GetLastSystemError(); + return GetLastSystemErrorAsFailure(nameof(OpenProcess), "Process opening"); return handle; } @@ -40,20 +43,20 @@ public Result OpenProcess(int pid) /// /// Identifier of the target process. /// A result holding either a boolean indicating if the process is 64-bit, or a system failure. - public Result IsProcess64Bit(int pid) + public Result IsProcess64Bit(int pid) { try { using var process = Process.GetProcessById(pid); if (!IsWow64Process(process.Handle, out bool isWow64)) - return GetLastSystemError(); + return GetLastSystemErrorAsFailure(nameof(IsWow64Process), "Process bitness check"); // Process is 64-bit if we are running a 64-bit system and the process is NOT in wow64. return !isWow64 && IsSystem64Bit(); } catch (Exception) { - return new SystemFailureOnInvalidArgument(nameof(pid), + return new InvalidArgumentFailure(nameof(pid), $"The process of PID {pid} was not found. Check that the process is running."); } } @@ -72,16 +75,17 @@ public Result IsProcess64Bit(int pid) /// Length of the memory range to read. /// A result holding either an array of bytes containing the data read from the process memory, or a /// system failure. - public Result ReadProcessMemory(IntPtr processHandle, UIntPtr baseAddress, ulong length) + public Result ReadProcessMemory(IntPtr processHandle, UIntPtr baseAddress, ulong length) { if (processHandle == IntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(processHandle), - "The process handle is invalid (zero pointer)."); + return new InvalidArgumentFailure(nameof(processHandle), "The process handle is invalid (zero pointer)."); var result = new byte[length]; int returnValue = ReadProcessMemory(processHandle, baseAddress, result, length, out _); - return returnValue == 0 ? GetLastSystemError() : result; + return returnValue == 0 ? + GetLastSystemErrorAsFailure(nameof(ReadProcessMemory), "Process memory reading") + : result; } /// @@ -97,17 +101,16 @@ public Result ReadProcessMemory(IntPtr processHandle, UIn /// Offset in the buffer where the data will be stored. /// Length of the memory range to read. /// A result holding either the number of bytes actually read from memory, or a system failure. - public Result ReadProcessMemoryPartial(IntPtr processHandle, UIntPtr baseAddress, + public Result ReadProcessMemoryPartial(IntPtr processHandle, UIntPtr baseAddress, byte[] buffer, int offset, ulong length) { if (processHandle == IntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(processHandle), - "The process handle is invalid (zero pointer)."); + return new InvalidArgumentFailure(nameof(processHandle), "The process handle is invalid (zero pointer)."); if ((ulong)buffer.Length < (ulong)offset + length) - return new SystemFailureOnInvalidArgument(nameof(buffer), + return new InvalidArgumentFailure(nameof(buffer), "The buffer is too small to store the requested number of bytes."); if (UIntPtr.MaxValue.ToUInt64() - baseAddress.ToUInt64() < length) - return new SystemFailureOnInvalidArgument(nameof(length), + return new InvalidArgumentFailure(nameof(length), "The base address plus the length to read exceeds the maximum possible address."); // We need to take in account the offset, meaning we can only write to the buffer from a certain position, @@ -133,7 +136,8 @@ public Result ReadProcessMemoryPartial(IntPtr processHandl // If the function is a success or read at least one byte, we return the number of bytes read. if (bytesRead != UIntPtr.Zero || returnValue != 0) return bytesRead.ToUInt64(); - var initialReadError = GetLastSystemError(); + var initialReadError = GetLastSystemErrorAsFailure(nameof(ReadProcessMemory), + "Partial process memory reading"); // If we are here, we know that the function failed, and also didn't read anything. // This may mean that the whole range is unreadable, but might also mean that only part of it is. @@ -149,7 +153,7 @@ public Result ReadProcessMemoryPartial(IntPtr processHandl // Determine if the process is 64-bit if (!IsWow64Process(processHandle, out bool isWow64)) - return GetLastSystemError(); + return GetLastSystemErrorAsFailure(nameof(IsWow64Process), "Partial process memory reading"); bool is64Bit = !isWow64 && IsSystem64Bit(); // Build the memory range that spans across everything we attempted to read @@ -169,7 +173,7 @@ public Result ReadProcessMemoryPartial(IntPtr processHandl // If the function failed again and didn't read any byte again, return the read error. if (bytesRead == UIntPtr.Zero && returnValue == 0) - return GetLastSystemError(); + return GetLastSystemErrorAsFailure(nameof(ReadProcessMemory), "Partial process memory reading"); // In other cases, we return the number of bytes read. return bytesRead.ToUInt64(); @@ -233,21 +237,20 @@ public Result ReadProcessMemoryPartial(IntPtr processHandl /// An address in the target page. /// New protection value for the page. /// A result holding either the memory protection value that was effective on the page before being - /// changed, or a system failure. - public Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bit, + /// changed, or a failure. + public Result ReadAndOverwriteProtection(IntPtr processHandle, bool is64Bit, UIntPtr targetAddress, MemoryProtection newProtection) { if (processHandle == IntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(processHandle), - "The process handle is invalid (zero pointer)."); + return new InvalidArgumentFailure(nameof(processHandle), "The process handle is invalid (zero pointer)."); if (targetAddress == UIntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(targetAddress), - "The target address cannot be a zero pointer."); + return new InvalidArgumentFailure(nameof(targetAddress), "The target address cannot be a zero pointer."); var result = VirtualProtectEx(processHandle, targetAddress, is64Bit ? 8 : 4, newProtection, out var previousProtection); - return result ? previousProtection : GetLastSystemError(); + return result ? previousProtection : GetLastSystemErrorAsFailure(nameof(VirtualProtectEx), + "Memory protection overwriting"); } /// @@ -257,20 +260,19 @@ public Result ReadAndOverwriteProtection(IntPtr /// PROCESS_VM_OPERATION access. /// Base address in the memory of the process to which data will be written. /// Bytes to write in the process memory. - /// A result indicating either a success or a system failure. - public Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, Span value) + /// A result indicating either a success or a failure. + public Result WriteProcessMemory(IntPtr processHandle, UIntPtr targetAddress, Span value) { if (processHandle == IntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(processHandle), - "The process handle is invalid (zero pointer)."); + return new InvalidArgumentFailure(nameof(processHandle), "The process handle is invalid (zero pointer)."); if (targetAddress == UIntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(targetAddress), - "The target address cannot be a zero pointer."); + return new InvalidArgumentFailure(nameof(targetAddress), "The target address cannot be a zero pointer."); var result = WriteProcessMemory(processHandle, targetAddress, ref value.GetPinnableReference(), (UIntPtr)value.Length, out _); - return result ? Result.Success : GetLastSystemError(); + return result ? Result.Success + : GetLastSystemErrorAsFailure(nameof(WriteProcessMemory), "Process memory writing"); } /// @@ -280,8 +282,8 @@ public Result WriteProcessMemory(IntPtr processHandle, UIntPtr ta /// Size in bytes of the memory to allocate. /// Type of memory allocation. /// Protection flags of the memory to allocate. - /// A result holding either a pointer to the start of the allocated memory, or a system failure. - public Result AllocateMemory(IntPtr processHandle, int size, + /// A result holding either a pointer to the start of the allocated memory, or a failure. + public Result AllocateMemory(IntPtr processHandle, int size, MemoryAllocationType allocationType, MemoryProtection protection) => AllocateMemory(processHandle, UIntPtr.Zero, size, allocationType, protection); @@ -293,18 +295,19 @@ public Result AllocateMemory(IntPtr processHandle, int s /// Size in bytes of the memory to allocate. /// Type of memory allocation. /// Protection flags of the memory to allocate. - /// A result holding either a pointer to the start of the allocated memory, or a system failure. - public Result AllocateMemory(IntPtr processHandle, UIntPtr address, int size, + /// A result holding either a pointer to the start of the allocated memory, or a failure. + public Result AllocateMemory(IntPtr processHandle, UIntPtr address, int size, MemoryAllocationType allocationType, MemoryProtection protection) { if (processHandle == IntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(processHandle), - "The process handle is invalid (zero pointer)."); + return new InvalidArgumentFailure(nameof(processHandle), "The process handle is invalid (zero pointer)."); if (size <= 0) - return new SystemFailureOnInvalidArgument(nameof(size), "The size to allocate must be strictly positive."); + return new InvalidArgumentFailure(nameof(size), "The size to allocate must be strictly positive."); var result = VirtualAllocEx(processHandle, address, (uint)size, (uint)allocationType, (uint)protection); - return result == UIntPtr.Zero ? GetLastSystemError() : result; + return result == UIntPtr.Zero ? + GetLastSystemErrorAsFailure(nameof(VirtualAllocEx), "Process memory allocation") + : result; } /// @@ -313,15 +316,15 @@ public Result AllocateMemory(IntPtr processHandle, UIntP /// Name or path of the module. This module must be loaded in the current process. /// Name of the target function in the specified module. /// A result holding the address of the function if located, or a system failure otherwise. - private Result GetFunctionAddress(string moduleName, string functionName) + private Result GetFunctionAddress(string moduleName, string functionName) { var moduleHandle = GetModuleHandle(moduleName); if (moduleHandle == IntPtr.Zero) - return GetLastSystemError(); + return GetLastSystemErrorAsFailure(nameof(GetModuleHandle), "Function address retrieval"); var functionAddress = GetProcAddress(moduleHandle, functionName); if (functionAddress == UIntPtr.Zero) - return GetLastSystemError(); + return GetLastSystemErrorAsFailure(nameof(GetProcAddress), "Function address retrieval"); return functionAddress; } @@ -333,19 +336,17 @@ private Result GetFunctionAddress(string moduleName, str /// Address of the start routine to be executed by the thread. /// Address of any parameter to be passed to the start routine. /// A result holding either the handle of the thread, or a system failure. - public Result CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, + public Result CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, UIntPtr parameterAddress) { if (processHandle == IntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(processHandle), - "The process handle is invalid (zero pointer)."); + return new InvalidArgumentFailure(nameof(processHandle), "The process handle is invalid (zero pointer)."); if (startAddress == UIntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(startAddress), - "The start address is invalid (zero pointer)."); + return new InvalidArgumentFailure(nameof(startAddress), "The start address is invalid (zero pointer)."); var result = CreateRemoteThread(processHandle, IntPtr.Zero, 0, startAddress, parameterAddress, 0, out _); if (result == IntPtr.Zero) - return GetLastSystemError(); + return GetLastSystemErrorAsFailure(nameof(CreateRemoteThread), "Remote thread creation"); return result; } @@ -355,26 +356,23 @@ public Result CreateRemoteThread(IntPtr processHandle, UI /// /// Handle of the target thread. /// Maximum time to wait for the thread to finish. - /// A result holding either the exit code of the thread, or a thread failure when the operation failed. - /// - public Result WaitThread(IntPtr threadHandle, TimeSpan timeout) + /// A result holding either the exit code of the thread, or a failure. + public Result WaitThread(IntPtr threadHandle, TimeSpan timeout) { if (threadHandle == IntPtr.Zero) - return new ThreadFailureOnInvalidArguments("The thread handle is invalid (zero pointer)."); + return new InvalidArgumentFailure(nameof(threadHandle), "The thread handle is invalid (zero pointer)."); uint result = WaitForSingleObject(threadHandle, (uint)timeout.TotalMilliseconds); if (result == WaitForSingleObjectResult.Failed) - return new ThreadFailureOnSystemFailure("Failed to wait for the thread to finish execution.", - GetLastSystemError()); + return GetLastSystemErrorAsFailure(nameof(WaitForSingleObject), "Thread waiting"); if (result == WaitForSingleObjectResult.Timeout) - return new ThreadFailureOnWaitTimeout(); + return new ThreadWaitTimeoutFailure(); if (!WaitForSingleObjectResult.IsSuccessful(result)) - return new ThreadFailureOnWaitAbandoned(); + return new ThreadWaitTimeoutFailure(); var exitCodeResult = GetExitCodeThread(threadHandle, out uint exitCode); if (!exitCodeResult) - return new ThreadFailureOnSystemFailure("Failed to get the exit code of the thread.", - GetLastSystemError()); + return GetLastSystemErrorAsFailure(nameof(GetExitCodeThread), "Thread waiting"); return exitCode; } @@ -385,17 +383,16 @@ public Result WaitThread(IntPtr threadHandle, TimeSpan time /// Base address of the region or placeholder to free, as returned by the memory /// allocation methods. /// A result indicating either a success or a system failure. - public Result ReleaseMemory(IntPtr processHandle, UIntPtr regionBaseAddress) + public Result ReleaseMemory(IntPtr processHandle, UIntPtr regionBaseAddress) { if (processHandle == IntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(processHandle), - "The process handle is invalid (zero pointer)."); + return new InvalidArgumentFailure(nameof(processHandle), "The process handle is invalid (zero pointer)."); if (regionBaseAddress == UIntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(regionBaseAddress), + return new InvalidArgumentFailure(nameof(regionBaseAddress), "The region base address is invalid (zero pointer)."); return VirtualFreeEx(processHandle, regionBaseAddress, 0, (uint)MemoryFreeType.Release) - ? Result.Success : GetLastSystemError(); + ? Result.Success : GetLastSystemErrorAsFailure(nameof(VirtualFreeEx), "Memory release"); } /// @@ -403,12 +400,13 @@ public Result ReleaseMemory(IntPtr processHandle, UIntPtr regionB /// /// Handle to close. /// A result indicating either a success or a system failure. - public Result CloseHandle(IntPtr handle) + public Result CloseHandle(IntPtr handle) { if (handle == IntPtr.Zero) - return new SystemFailureOnInvalidArgument(nameof(handle), "The handle is invalid (zero pointer)."); + return new InvalidArgumentFailure(nameof(handle), "The handle is invalid (zero pointer)."); - return WinCloseHandle(handle) ? Result.Success : GetLastSystemError(); + return WinCloseHandle(handle) ? Result.Success + : GetLastSystemErrorAsFailure(nameof(WinCloseHandle), "Handle closing"); } /// @@ -445,7 +443,7 @@ public MemoryRange GetFullMemoryRange() /// Handle of the target process. /// Base address of the target memory region. /// A result holding either the metadata of the target memory region, or a system failure. - public Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress) + public Result GetRegionMetadata(IntPtr processHandle, UIntPtr baseAddress) { MemoryBasicInformation memoryBasicInformation; if (IsSystem64Bit()) @@ -453,8 +451,8 @@ public Result GetRegionMetadata(IntPtr proce // Use the 64-bit variant of the structure. var memInfo64 = new MemoryBasicInformation64(); if (VirtualQueryEx(processHandle, baseAddress, out memInfo64, - (UIntPtr)Marshal.SizeOf(memInfo64)) == UIntPtr.Zero) - return GetLastSystemError(); + (UIntPtr)Marshal.SizeOf(memInfo64)) == UIntPtr.Zero) + return GetLastSystemErrorAsFailure(nameof(VirtualQueryEx), "Memory region metadata retrieval"); memoryBasicInformation = new MemoryBasicInformation((UIntPtr)memInfo64.BaseAddress, (UIntPtr)memInfo64.AllocationBase, memInfo64.AllocationProtect, (UIntPtr)memInfo64.RegionSize, @@ -464,12 +462,12 @@ public Result GetRegionMetadata(IntPtr proce { // Use the 32-bit variant of the structure. var memInfo32 = new MemoryBasicInformation32(); - if (VirtualQueryEx(processHandle, baseAddress, out memInfo32, - (UIntPtr)Marshal.SizeOf(memInfo32)) == UIntPtr.Zero) - return GetLastSystemError(); + if (VirtualQueryEx(processHandle, baseAddress, out memInfo32, + (UIntPtr)Marshal.SizeOf(memInfo32)) == UIntPtr.Zero) + return GetLastSystemErrorAsFailure(nameof(VirtualQueryEx), "Memory region metadata retrieval"); - memoryBasicInformation = new MemoryBasicInformation((UIntPtr)memInfo32.BaseAddress, - (UIntPtr)memInfo32.AllocationBase, memInfo32.AllocationProtect, (UIntPtr)memInfo32.RegionSize, + memoryBasicInformation = new MemoryBasicInformation(memInfo32.BaseAddress, + memInfo32.AllocationBase, memInfo32.AllocationProtect, memInfo32.RegionSize, memInfo32.State, memInfo32.Protect, memInfo32.Type); } diff --git a/src/MindControl/ProcessException.cs b/src/MindControl/ProcessException.cs deleted file mode 100644 index ac87978..0000000 --- a/src/MindControl/ProcessException.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace MindControl; - -/// -/// Exception related to handling processes. -/// -public class ProcessException : Exception -{ - /// - /// Gets the identifier of the process related to the exception, if any. - /// - public int? Pid { get; } - - /// - /// Builds a with the given details. - /// - /// Error message that explains the reason for the exception. - public ProcessException(string message) : this(null, message, null) {} - - /// - /// Builds a with the given details. - /// - /// PID (process identifier) of the process related to the exception, if any. - /// Error message that explains the reason for the exception. - public ProcessException(int? pid, string message) : this(pid, message, null) {} - - /// - /// Builds a with the given details. - /// - /// PID (process identifier) of the process related to the exception, if any. - /// Error message that explains the reason for the exception. - /// Exception that is the cause of this exception, if any. - public ProcessException(int? pid, string message, Exception? innerException) : base(message, innerException) - { - Pid = pid; - } -} \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs index d2d5fce..c04c4e7 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using MindControl.Internal; using MindControl.Modules; using MindControl.Results; @@ -7,33 +6,33 @@ namespace MindControl; // This partial class implements methods related to retrieving memory addresses. public partial class ProcessMemory -{ +{ /// /// Evaluates the given pointer path to the memory address it points to in the process. /// /// Pointer path to evaluate. /// The memory address pointed by the pointer path. - public Result EvaluateMemoryAddress(PointerPath pointerPath) + public Result EvaluateMemoryAddress(PointerPath pointerPath) { if (!IsAttached) - return new PathEvaluationFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (pointerPath.IsStrictly64Bit && (IntPtr.Size == 4 || !Is64Bit)) - return new PathEvaluationFailureOnIncompatibleBitness(); + return new IncompatiblePointerPathBitnessFailure(); UIntPtr? baseAddress; if (pointerPath.BaseModuleName != null) { - baseAddress = GetModuleAddress(pointerPath.BaseModuleName); + baseAddress = GetModule(pointerPath.BaseModuleName)?.GetRange().Start; if (baseAddress == null) - return new PathEvaluationFailureOnBaseModuleNotFound(pointerPath.BaseModuleName); + return new BaseModuleNotFoundFailure(pointerPath.BaseModuleName); } else { var firstOffset = pointerPath.PointerOffsets.FirstOrDefault(); baseAddress = firstOffset.AsAddress(); if (baseAddress == null) - return new PathEvaluationFailureOnPointerOutOfRange(null, firstOffset); + return new PointerOutOfRangeFailure(null, firstOffset); } // Apply the base offset if there is one @@ -41,16 +40,16 @@ public Result EvaluateMemoryAddress(PointerPath { var baseAddressWithOffset = pointerPath.BaseModuleOffset.OffsetAddress(baseAddress.Value); if (baseAddressWithOffset == null) - return new PathEvaluationFailureOnPointerOutOfRange(baseAddress, pointerPath.BaseModuleOffset); + return new PointerOutOfRangeFailure(baseAddress, pointerPath.BaseModuleOffset); baseAddress = baseAddressWithOffset.Value; } // Check if the base address is valid if (baseAddress == UIntPtr.Zero) - return new PathEvaluationFailureOnPointerOutOfRange(UIntPtr.Zero, PointerOffset.Zero); + return new PointerOutOfRangeFailure(UIntPtr.Zero, PointerOffset.Zero); if (!IsBitnessCompatible(baseAddress.Value)) - return new PathEvaluationFailureOnIncompatibleBitness(baseAddress.Value); + return new IncompatibleBitnessPointerFailure(baseAddress.Value); // Follow the pointer path offset by offset var currentAddress = baseAddress.Value; @@ -60,7 +59,7 @@ public Result EvaluateMemoryAddress(PointerPath // Read the value pointed by the current address as a pointer address var nextAddressResult = Read(currentAddress); if (nextAddressResult.IsFailure) - return new PathEvaluationFailureOnPointerReadFailure(currentAddress, nextAddressResult.Error); + return nextAddressResult.Failure; var nextAddress = nextAddressResult.Value; @@ -70,9 +69,9 @@ public Result EvaluateMemoryAddress(PointerPath // Check for invalid address values if (nextValue == null || nextValue.Value == UIntPtr.Zero) - return new PathEvaluationFailureOnPointerOutOfRange(nextAddress, offset); + return new PointerOutOfRangeFailure(nextAddress, offset); if (!IsBitnessCompatible(nextValue.Value)) - return new PathEvaluationFailureOnIncompatibleBitness(nextAddress); + return new IncompatibleBitnessPointerFailure(nextAddress); // The next value has been vetted. Keep going with it as the current address currentAddress = nextValue.Value; @@ -89,15 +88,33 @@ public Result EvaluateMemoryAddress(PointerPath /// /// Pointer path to the starting address of the stream. /// A result holding either the created process memory stream, or a path evaluation failure. - public DisposableResult GetMemoryStream(PointerPath pointerPath) + public DisposableResult GetMemoryStream(PointerPath pointerPath) { var addressResult = EvaluateMemoryAddress(pointerPath); if (addressResult.IsFailure) - return addressResult.Error; + return addressResult.Failure; return GetMemoryStream(addressResult.Value); } + private Dictionary? _cachedModules; + + /// + /// Refreshes the module cache used when evaluating pointer paths. + /// Call this method when your process' modules have changed due to external factors such as plugins being loaded, + /// or other third-party software interacting with the process. + /// + public void RefreshModuleCache() + { + using var process = GetAttachedProcessInstance(); + if (process.IsFailure) + return; + + _cachedModules = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (ProcessModule module in process.Value.Modules) + _cachedModules[module.ModuleName] = new RemoteModule(this, module); + } + /// /// Creates and returns a new instance that starts at the given address. /// The stream can be used to read or write into the process memory. It is owned by the caller and must be disposed @@ -107,33 +124,6 @@ public DisposableResult GetMemoryStr /// The created process memory stream. public ProcessMemoryStream GetMemoryStream(UIntPtr startAddress) => new(_osService, ProcessHandle, startAddress); - - /// - /// Gets the process module with the given name. - /// - /// Name of the target module. - /// The target process instance to get the module from. - /// The module if found, null otherwise. - private static ProcessModule? GetModule(string moduleName, Process process) - { - return process.Modules - .Cast() - .FirstOrDefault(m => string.Equals(m.ModuleName, moduleName, StringComparison.OrdinalIgnoreCase)); - } - - /// - /// Gets the base address of the process module with the given name. - /// - /// Name of the target module. - /// The base address of the module if found, null otherwise. - public UIntPtr? GetModuleAddress(string moduleName) - { - using var process = GetAttachedProcessInstance(); - var module = GetModule(moduleName, process); - IntPtr? baseAddress = module?.BaseAddress; - - return baseAddress == null ? null : (UIntPtr)(long)baseAddress; - } /// /// Gets the module with the given name, if it exists. @@ -142,9 +132,10 @@ public ProcessMemoryStream GetMemoryStream(UIntPtr startAddress) /// The module if found, null otherwise. public RemoteModule? GetModule(string moduleName) { - using var process = GetAttachedProcessInstance(); - var module = GetModule(moduleName, process); - return module == null ? null : new RemoteModule(this, module); + if (_cachedModules == null) + RefreshModuleCache(); + + return _cachedModules!.GetValueOrDefault(moduleName); } /// diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index d6f32a8..417d8fc 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -23,20 +23,20 @@ public partial class ProcessMemory /// Determines if the memory range can be used to store executable code. /// Specify this parameter to limit the allocation to a specific range of memory. /// If specified, try to allocate as close as possible to this address. - /// A result holding either the allocated memory range, or an allocation failure. - public DisposableResult Allocate(ulong size, bool forExecutableCode, + /// A result holding either the allocated memory range, or a failure. + public DisposableResult Allocate(ulong size, bool forExecutableCode, MemoryRange? limitRange = null, UIntPtr? nearAddress = null) { if (!IsAttached) - return new AllocationFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (size == 0) - return new AllocationFailureOnInvalidArguments( + return new InvalidArgumentFailure(nameof(size), "The size of the memory range to allocate must be greater than zero."); // Find a free memory range that satisfies the size needed var rangeResult = FindAndAllocateFreeMemory(size, forExecutableCode, limitRange, nearAddress); if (rangeResult.IsFailure) - return rangeResult.Error; + return rangeResult.Failure; // Add the range to the list of allocated ranges and return it var allocatedRange = new MemoryAllocation(rangeResult.Value, forExecutableCode, this); @@ -67,10 +67,10 @@ internal void Free(MemoryAllocation allocation) /// Specify this parameter to limit the search to a specific range of memory. /// If left null (default), the entire process memory will be searched. /// If specified, try to allocate as close as possible to this address. - /// A result holding either the memory range found, or an allocation failure. + /// A result holding either the memory range found, or a failure. /// The reason why the method performs the allocation itself is because we cannot know if the range can /// actually be allocated without performing the allocation. - private Result FindAndAllocateFreeMemory(ulong sizeNeeded, + private Result FindAndAllocateFreeMemory(ulong sizeNeeded, bool forExecutableCode, MemoryRange? limitRange = null, UIntPtr? nearAddress = null) { var maxRange = _osService.GetFullMemoryRange(); @@ -78,7 +78,7 @@ private Result FindAndAllocateFreeMemory(ulong s // If the given range is not within the process applicative memory, return null if (actualRange == null) - return new AllocationFailureOnLimitRangeOutOfBounds(maxRange); + return new LimitRangeOutOfBoundsFailure(maxRange); // Compute the minimum multiple of the system page size that can fit the size needed // This will be the maximum size that we are going to allocate @@ -100,7 +100,7 @@ private Result FindAndAllocateFreeMemory(ulong s while ((nextAddress.ToUInt64() <= actualRange.Value.End.ToUInt64() && nextAddress.ToUInt64() >= actualRange.Value.Start.ToUInt64()) && (currentMetadata = _osService.GetRegionMetadata(ProcessHandle, nextAddress) - .GetValueOrDefault()).Size.ToUInt64() > 0) + .ValueOrDefault()).Size.ToUInt64() > 0) { nextAddressForward = (UIntPtr)Math.Max(nextAddressForward.ToUInt64(), nextAddress.ToUInt64() + currentMetadata.Size.ToUInt64()); @@ -177,7 +177,7 @@ private Result FindAndAllocateFreeMemory(ulong s } // We reached the end of the memory range and didn't find a suitable free range. - return new AllocationFailureOnNoFreeMemoryFound(actualRange.Value, nextAddress); + return new NoFreeMemoryFailure(actualRange.Value, nextAddress); } #region Store @@ -194,14 +194,14 @@ private Result FindAndAllocateFreeMemory(ulong s /// /// If specified, prioritize allocations by their proximity to this address. If no /// matching allocation is found, a new allocation as close as possible to this address will be attempted. - /// A result holding either the resulting reservation, or an allocation failure. - public DisposableResult Reserve(ulong size, bool requireExecutable, + /// A result holding either the resulting reservation, or a failure. + public DisposableResult Reserve(ulong size, bool requireExecutable, MemoryRange? limitRange = null, UIntPtr? nearAddress = null) { if (!IsAttached) - return new AllocationFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (size == 0) - return new AllocationFailureOnInvalidArguments( + return new InvalidArgumentFailure(nameof(size), "The size of the memory range to reserve must be greater than zero."); uint alignment = Is64Bit ? (uint)8 : 4; @@ -226,7 +226,7 @@ public DisposableResult Reserve(ulong size // No allocation could satisfy the reservation: allocate a new range var allocationResult = Allocate(size, requireExecutable, limitRange, nearAddress); if (allocationResult.IsFailure) - return allocationResult.Error; + return allocationResult.Failure; // Make a reservation within that new allocation var newAllocation = allocationResult.Value; @@ -236,7 +236,7 @@ public DisposableResult Reserve(ulong size // There is no reason for the reservation to fail here, as we just allocated memory of sufficient size. // Just in case, we free the memory and return the most appropriate failure. Free(allocationResult.Value); - return new AllocationFailureOnNoFreeMemoryFound(newAllocation.Range, newAllocation.Range.Start); + return new NoFreeMemoryFailure(newAllocation.Range, newAllocation.Range.Start); } return reservationResult.Value; @@ -249,16 +249,16 @@ public DisposableResult Reserve(ulong size /// Data to store. /// Set to true if the data is executable code. Defaults to false. /// A result holding either the reserved memory range, or an allocation failure. - public DisposableResult Store(byte[] data, bool isCode = false) + public DisposableResult Store(byte[] data, bool isCode = false) { if (!IsAttached) - return new StoreFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (data.Length == 0) - return new StoreFailureOnInvalidArguments("The data to store must not be empty."); + return new InvalidArgumentFailure(nameof(data), "The data to store must not be empty."); var reservedRangeResult = Reserve((ulong)data.Length, isCode); if (reservedRangeResult.IsFailure) - return new StoreFailureOnAllocation(reservedRangeResult.Error); + return reservedRangeResult.Failure; var reservedRange = reservedRangeResult.Value; var writeResult = WriteBytes(reservedRange.Range.Start, data, MemoryProtectionStrategy.Ignore); @@ -266,7 +266,7 @@ public DisposableResult Store(byte[] data, bool { // If writing the data failed, free the reservation and return the write failure. reservedRange.Dispose(); - return new StoreFailureOnWrite(writeResult.Error); + return writeResult.Failure; } return reservedRange; @@ -274,22 +274,22 @@ public DisposableResult Store(byte[] data, bool /// /// Stores the given data in the specified allocated range. Returns the reservation that holds the data. - /// In most situations, you should use the or signatures + /// In most situations, you can use the or signatures /// instead, to have the instance handle allocations automatically. Use this signature /// if you need to manage allocations and reservations manually. /// /// Data to store. /// Allocated memory to store the data. - /// A result holding either the reservation storing the data, or a reservation failure. - public DisposableResult Store(byte[] data, MemoryAllocation allocation) + /// A result holding either the reservation storing the data, or a failure. + public DisposableResult Store(byte[] data, MemoryAllocation allocation) { if (!IsAttached) - return new StoreFailureOnDetachedProcess(); + return new DetachedProcessFailure(); uint alignment = Is64Bit ? (uint)8 : 4; var reservedRangeResult = allocation.ReserveRange((ulong)data.Length, alignment); if (reservedRangeResult.IsFailure) - return new StoreFailureOnReservation(reservedRangeResult.Error); + return reservedRangeResult.Failure; var reservedRange = reservedRangeResult.Value; var writeResult = WriteBytes(reservedRange.Range.Start, data, MemoryProtectionStrategy.Ignore); @@ -297,7 +297,7 @@ public DisposableResult Store(byte[] data, Memo { // If writing the data failed, free the reservation and return the write failure. reservedRange.Dispose(); - return new StoreFailureOnWrite(writeResult.Error); + return writeResult.Failure; } return reservedRange; @@ -309,24 +309,22 @@ public DisposableResult Store(byte[] data, Memo /// /// Value or structure to store. /// Type of the value or structure. - /// A result holding either the reservation where the data has been written, or an allocation failure. - /// - public DisposableResult Store(T value) + /// A result holding either the reservation where the data has been written, or a failure. + public DisposableResult Store(T value) => Store(value.ToBytes(), false); /// /// Stores the given value or structure in the specified range of memory. Returns the reservation that holds the /// data. - /// In most situations, you should use the or signatures + /// In most situations, you can use the or signatures /// instead, to have the instance handle allocations automatically. Use this signature /// if you need to manage allocations and reservations manually. /// /// Value or structure to store. /// Range of memory to store the data in. /// Type of the value or structure. - /// A result holding either the reservation where the data has been written, or an allocation failure. - /// - public DisposableResult Store(T value, MemoryAllocation allocation) + /// A result holding either the reservation where the data has been written, or a failure. + public DisposableResult Store(T value, MemoryAllocation allocation) where T: struct => Store(value.ToBytes(), allocation); @@ -336,18 +334,17 @@ public DisposableResult Store(T value, Memor /// /// String to store. /// String settings to use to write the string. - /// A result holding either the reservation where the string has been written, or an allocation failure. - /// - public DisposableResult StoreString(string value, StringSettings settings) + /// A result holding either the reservation where the string has been written, or a failure. + public DisposableResult StoreString(string value, StringSettings settings) { if (!IsAttached) - return new StoreFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (!settings.IsValid) - return new StoreFailureOnInvalidArguments(StringSettings.InvalidSettingsMessage); + return new InvalidArgumentFailure(nameof(settings), StringSettings.InvalidSettingsMessage); var bytes = settings.GetBytes(value); if (bytes == null) - return new StoreFailureOnInvalidArguments(StringSettings.GetBytesFailureMessage); + return new InvalidArgumentFailure(nameof(settings), StringSettings.GetBytesFailureMessage); return Store(bytes, isCode: false); } @@ -361,19 +358,18 @@ public DisposableResult StoreString(string valu /// String to store. /// String settings to use to write the string. /// Range of memory to store the string in. - /// A result holding either the reservation where the string has been written, or an allocation failure. - /// - public DisposableResult StoreString(string value, StringSettings settings, + /// A result holding either the reservation where the string has been written, or a failure. + public DisposableResult StoreString(string value, StringSettings settings, MemoryAllocation allocation) { if (!IsAttached) - return new StoreFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (!settings.IsValid) - return new StoreFailureOnInvalidArguments(StringSettings.InvalidSettingsMessage); + return new InvalidArgumentFailure(nameof(settings), StringSettings.InvalidSettingsMessage); var bytes = settings.GetBytes(value); if (bytes == null) - return new StoreFailureOnInvalidArguments(StringSettings.GetBytesFailureMessage); + return new InvalidArgumentFailure(nameof(settings), StringSettings.GetBytesFailureMessage); return Store(bytes, allocation); } diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs b/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs index d76dc3c..14690c3 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Anchors.cs @@ -1,5 +1,4 @@ using MindControl.Anchors; -using MindControl.Results; namespace MindControl; @@ -14,11 +13,11 @@ public partial class ProcessMemory /// Address of the value in memory. /// Type of the value to read and write. /// An anchor for the value at the specified address. - public ValueAnchor GetAnchor(UIntPtr address) + public ValueAnchor GetAnchor(UIntPtr address) where T : struct { - var memoryAdapter = new GenericMemoryAdapter(new LiteralAddressResolver(address)); - return new ValueAnchor(memoryAdapter, this); + var memoryAdapter = new GenericMemoryAdapter(new LiteralAddressResolver(address)); + return new ValueAnchor(memoryAdapter, this); } /// @@ -29,11 +28,11 @@ public ValueAnchor GetAnchor(UIntPtr address) /// Pointer path to the address of the value in memory. /// Type of the value to read and write. /// An anchor for the value at the specified address. - public ValueAnchor GetAnchor(PointerPath pointerPath) + public ValueAnchor GetAnchor(PointerPath pointerPath) where T : struct { - var memoryAdapter = new GenericMemoryAdapter(new PointerPathResolver(pointerPath)); - return new ValueAnchor(memoryAdapter, this); + var memoryAdapter = new GenericMemoryAdapter(new PointerPathResolver(pointerPath)); + return new ValueAnchor(memoryAdapter, this); } /// @@ -43,10 +42,10 @@ public ValueAnchor GetAnchor(PointerPath pointe /// Address of target byte array in memory. /// Size of the target byte array. /// An anchor for the array at the specified address. - public ValueAnchor GetByteArrayAnchor(UIntPtr address, int size) + public ValueAnchor GetByteArrayAnchor(UIntPtr address, int size) { - var memoryAdapter = new ByteArrayMemoryAdapter(new LiteralAddressResolver(address), size); - return new ValueAnchor(memoryAdapter, this); + var memoryAdapter = new ByteArrayMemoryAdapter(new LiteralAddressResolver(address), size); + return new ValueAnchor(memoryAdapter, this); } /// @@ -56,11 +55,10 @@ public ValueAnchor GetByteArrayAnchor(UIntPtr /// Pointer path to the address of the target array in memory. /// Size of the target byte array. /// An anchor for the array at the specified address. - public ValueAnchor GetByteArrayAnchor(PointerPath pointerPath, int size) + public ValueAnchor GetByteArrayAnchor(PointerPath pointerPath, int size) { - var memoryAdapter = new ByteArrayMemoryAdapter( - new PointerPathResolver(pointerPath), size); - return new ValueAnchor(memoryAdapter, this); + var memoryAdapter = new ByteArrayMemoryAdapter(new PointerPathResolver(pointerPath), size); + return new ValueAnchor(memoryAdapter, this); } /// @@ -71,11 +69,10 @@ public ValueAnchor GetByteArrayAnchor(Pointer /// Address of the string pointer in memory. /// Settings to read the string. /// An anchor for the value at the specified address. - public ValueAnchor GetStringPointerAnchor(UIntPtr address, - StringSettings stringSettings) + public ValueAnchor GetStringPointerAnchor(UIntPtr address, StringSettings stringSettings) { - var memoryAdapter = new StringPointerMemoryAdapter(new LiteralAddressResolver(address), stringSettings); - return new ValueAnchor(memoryAdapter, this); + var memoryAdapter = new StringPointerMemoryAdapter(new LiteralAddressResolver(address), stringSettings); + return new ValueAnchor(memoryAdapter, this); } /// @@ -86,11 +83,9 @@ public ValueAnchor GetStringPoin /// Pointer path to the address of the string pointer in memory. /// Settings to read the string. /// An anchor for the value at the specified address. - public ValueAnchor GetStringPointerAnchor( - PointerPath pointerPath, StringSettings stringSettings) + public ValueAnchor GetStringPointerAnchor(PointerPath pointerPath, StringSettings stringSettings) { - var memoryAdapter = new StringPointerMemoryAdapter( - new PointerPathResolver(pointerPath), stringSettings); - return new ValueAnchor(memoryAdapter, this); + var memoryAdapter = new StringPointerMemoryAdapter(new PointerPathResolver(pointerPath), stringSettings); + return new ValueAnchor(memoryAdapter, this); } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs b/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs index cc758fe..5637a7a 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.FindBytes.cs @@ -137,7 +137,7 @@ private IEnumerable ScanRangeForBytePattern(byte[] bytePattern, byte[] { // Read the whole memory range and place it in a byte array. byte[] rangeMemory = _osService.ReadProcessMemory(ProcessHandle, range.Start, range.GetSize()) - .GetValueOrDefault() ?? Array.Empty(); + .ValueOrDefault() ?? Array.Empty(); int maxIndex = rangeMemory.Length - bytePattern.Length; diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs b/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs index 420edd0..dfe1d3c 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Injection.cs @@ -13,53 +13,55 @@ public partial class ProcessMemory /// Injects a library into the attached process. /// /// Path to the library file to inject into the process. - /// A successful result, or an injection failure detailing how the operation failed. - public Result InjectLibrary(string libraryPath) - => InjectLibrary(libraryPath, DefaultLibraryInjectionThreadTimeout); + /// A result indicating success or failure. + public Result InjectLibrary(string libraryPath) => InjectLibrary(libraryPath, DefaultLibraryInjectionThreadTimeout); /// /// Injects a library into the attached process. /// /// Path to the library file to inject into the process. /// Time to wait for the injection thread to return. - /// A successful result, or an injection failure detailing how the operation failed. - public Result InjectLibrary(string libraryPath, TimeSpan waitTimeout) + /// A result indicating success or failure. + public Result InjectLibrary(string libraryPath, TimeSpan waitTimeout) { if (!IsAttached) - return new InjectionFailureOnDetachedProcess(); + return new DetachedProcessFailure(); // Check if the library file exists string absoluteLibraryPath = Path.GetFullPath(libraryPath); if (!File.Exists(absoluteLibraryPath)) - return new InjectionFailureOnLibraryFileNotFound(absoluteLibraryPath); + return new LibraryFileNotFoundFailure(absoluteLibraryPath); // Check if the module is already loaded string expectedModuleName = Path.GetFileName(libraryPath); - if (GetModuleAddress(expectedModuleName) != null) - return new InjectionFailureOnModuleAlreadyLoaded(); + if (GetModule(expectedModuleName) != null) + return new ModuleAlreadyLoadedFailure(); // Store the library path string in the target process memory var reservationResult = StoreString(absoluteLibraryPath, new StringSettings(Encoding.Unicode)); if (reservationResult.IsFailure) - return new InjectionFailureOnParameterStorage(reservationResult.Error); + return reservationResult; var reservation = reservationResult.Value; // Run LoadLibraryW from inside the target process to have it load the library itself, which is usually safer using var threadResult = RunThread("kernel32.dll", "LoadLibraryW", reservation.Address); if (threadResult.IsFailure) - return new InjectionFailureOnThreadFailure(threadResult.Error); + return threadResult.Failure; // Wait for the thread to return var waitResult = threadResult.Value.WaitForCompletion(waitTimeout); if (waitResult.IsFailure) - return new InjectionFailureOnThreadFailure(waitResult.Error); + return waitResult; // The exit code of the thread should be a handle to the loaded module. // We don't need the handle, but we can use it to verify that the library was loaded successfully. var moduleHandle = waitResult.Value; if (moduleHandle == 0) - return new InjectionFailureOnLoadLibraryFailure(); + return new LibraryLoadFailure(); - return Result.Success; + // Refresh the module cache so that the new module is usable + RefreshModuleCache(); + + return Result.Success; } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs index 9eeb5b3..aca235e 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Read.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Read.cs @@ -22,13 +22,13 @@ public partial class ProcessMemory /// /// Optimized, reusable path to the target address. /// Number of bytes to read. - /// The value read from the process memory, or a read failure. - public Result ReadBytes(PointerPath pointerPath, long length) + /// The value read from the process memory, or a failure. + public Result ReadBytes(PointerPath pointerPath, long length) { if (!IsAttached) - return new ReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (length < 0) - return new ReadFailureOnInvalidArguments("The length to read cannot be negative."); + return new InvalidArgumentFailure(nameof(length), "The length to read cannot be negative."); return ReadBytes(pointerPath, (ulong)length); } @@ -38,15 +38,14 @@ public Result ReadBytes(PointerPath pointerPath, long lengt /// /// Optimized, reusable path to the target address. /// Number of bytes to read. - /// The value read from the process memory, or a read failure. - public Result ReadBytes(PointerPath pointerPath, ulong length) + /// The value read from the process memory, or a failure. + public Result ReadBytes(PointerPath pointerPath, ulong length) { if (!IsAttached) - return new ReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadBytes(addressResult.Value, length) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + return addressResult.IsSuccess ? ReadBytes(addressResult.Value, length) : addressResult.Failure; } /// @@ -54,13 +53,13 @@ public Result ReadBytes(PointerPath pointerPath, ulong leng /// /// Target address in the process memory. /// Number of bytes to read. - /// The value read from the process memory, or a read failure. - public Result ReadBytes(UIntPtr address, long length) + /// The value read from the process memory, or a failure. + public Result ReadBytes(UIntPtr address, long length) { if (!IsAttached) - return new ReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (length < 0) - return new ReadFailureOnInvalidArguments("The length to read cannot be negative."); + return new InvalidArgumentFailure(nameof(length), "The length to read cannot be negative."); return ReadBytes(address, (ulong)length); } @@ -70,19 +69,17 @@ public Result ReadBytes(UIntPtr address, long length) /// /// Target address in the process memory. /// Number of bytes to read. - /// The value read from the process memory, or a read failure. - public Result ReadBytes(UIntPtr address, ulong length) + /// The value read from the process memory, or a failure. + public Result ReadBytes(UIntPtr address, ulong length) { if (!IsAttached) - return new ReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (address == UIntPtr.Zero) - return new ReadFailureOnZeroPointer(); + return new ZeroPointerFailure(); if (!IsBitnessCompatible(address)) - return new ReadFailureOnIncompatibleBitness(address); + return new IncompatibleBitnessPointerFailure(address); - var readResult = _osService.ReadProcessMemory(ProcessHandle, address, length); - return readResult.IsSuccess ? readResult.Value - : new ReadFailureOnSystemRead(readResult.Error); + return _osService.ReadProcessMemory(ProcessHandle, address, length); } /// @@ -95,15 +92,15 @@ public Result ReadBytes(UIntPtr address, ulong length) /// Pointer path to the target address in the process memory. /// Buffer to store the bytes read. /// Number of bytes to read, at most. - /// The value read from the process memory, or a read failure in case no bytes could be read. - public Result ReadBytesPartial(PointerPath pointerPath, byte[] buffer, ulong maxLength) + /// The value read from the process memory, or a failure in case no bytes could be read. + public Result ReadBytesPartial(PointerPath pointerPath, byte[] buffer, ulong maxLength) { if (!IsAttached) - return new ReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? ReadBytesPartial(addressResult.Value, buffer, maxLength) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + return addressResult.IsSuccess ? ReadBytesPartial(addressResult.Value, buffer, maxLength) + : addressResult.Failure; } /// @@ -116,23 +113,23 @@ public Result ReadBytesPartial(PointerPath pointerPath, byte /// Target address in the process memory. /// Buffer to store the bytes read. /// Number of bytes to read, at most. - /// The value read from the process memory, or a read failure in case no bytes could be read. - public Result ReadBytesPartial(UIntPtr address, byte[] buffer, ulong maxLength) + /// The value read from the process memory, or a failure in case no bytes could be read. + public Result ReadBytesPartial(UIntPtr address, byte[] buffer, ulong maxLength) { if (!IsAttached) - return new ReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (maxLength == 0) return 0; if ((ulong)buffer.Length < maxLength) - return new ReadFailureOnInvalidArguments("The buffer length must be at least the provided length to read."); + return new InvalidArgumentFailure(nameof(buffer), + "The buffer length must be at least the provided length to read."); if (address == UIntPtr.Zero) - return new ReadFailureOnZeroPointer(); + return new ZeroPointerFailure(); if (!IsBitnessCompatible(address)) - return new ReadFailureOnIncompatibleBitness(address); + return new IncompatibleBitnessPointerFailure(address); var readResult = _osService.ReadProcessMemoryPartial(ProcessHandle, address, buffer, 0, maxLength); - return readResult.IsSuccess ? readResult.Value - : new ReadFailureOnSystemRead(readResult.Error); + return readResult; } #endregion @@ -147,15 +144,14 @@ public Result ReadBytesPartial(UIntPtr address, byte[] buffe /// Example: "MyGame.exe+1F1688,1F,4". Reuse instances to optimize execution time. /// Type of data to read. Only value types are supported (primitive types and structures). /// - /// The value read from the process memory, or a read failure. - public Result Read(PointerPath pointerPath) where T : struct + /// The value read from the process memory, or a failure. + public Result Read(PointerPath pointerPath) where T : struct { if (!IsAttached) - return new ReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? Read(addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + return addressResult.IsSuccess ? Read(addressResult.Value) : addressResult.Failure; } /// @@ -165,15 +161,15 @@ public Result Read(PointerPath pointerPath) where T : struct /// Target address in the process memory. /// Type of data to read. Only value types are supported (primitive types and structures). /// - /// The value read from the process memory, or a read failure. - public Result Read(UIntPtr address) where T : struct + /// The value read from the process memory, or a failure. + public Result Read(UIntPtr address) where T : struct { if (!IsAttached) - return new ReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (address == UIntPtr.Zero) - return new ReadFailureOnZeroPointer(); + return new ZeroPointerFailure(); if (!IsBitnessCompatible(address)) - return new ReadFailureOnIncompatibleBitness(address); + return new IncompatibleBitnessPointerFailure(address); // Get the size of the target type int size; @@ -189,14 +185,14 @@ public Result Read(UIntPtr address) where T : struct } catch (ArgumentException) { - return new ReadFailureOnConversionFailure(); + return new ConversionFailure(); } } // Read the bytes from the process memory var readResult = _osService.ReadProcessMemory(ProcessHandle, address, (ulong)size); if (readResult.IsFailure) - return new ReadFailureOnSystemRead(readResult.Error); + return readResult.Failure; byte[] bytes = readResult.Value; // Convert the bytes into the target type @@ -210,7 +206,7 @@ public Result Read(UIntPtr address) where T : struct } catch (Exception) { - return new ReadFailureOnConversionFailure(); + return new ConversionFailure(); } } @@ -222,15 +218,14 @@ public Result Read(UIntPtr address) where T : struct /// /// Pointer path to the target address. Can be implicitly converted from a string. /// Example: "MyGame.exe+1F1688,1F,4". Reuse instances to optimize execution time. - /// The value read from the process memory, or a read failure. - public Result Read(Type type, PointerPath pointerPath) + /// The value read from the process memory, or a failure. + public Result Read(Type type, PointerPath pointerPath) { if (!IsAttached) - return new ReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? Read(type, addressResult.Value) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + return addressResult.IsSuccess ? Read(type, addressResult.Value) : addressResult.Failure; } /// @@ -240,17 +235,17 @@ public Result Read(Type type, PointerPath pointerPath) /// Type of data to read. Only value types are supported (primitive types and structures). /// /// Target address in the process memory. - /// The value read from the process memory, or a read failure. - public Result Read(Type type, UIntPtr address) + /// The value read from the process memory, or a failure. + public Result Read(Type type, UIntPtr address) { if (!IsAttached) - return new ReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (!type.IsValueType) - return new ReadFailureOnUnsupportedType(type); + return new UnsupportedTypeReadFailure(type); if (address == UIntPtr.Zero) - return new ReadFailureOnZeroPointer(); + return new ZeroPointerFailure(); if (!IsBitnessCompatible(address)) - return new ReadFailureOnIncompatibleBitness(address); + return new IncompatibleBitnessPointerFailure(address); // Get the size of the target type int size = Marshal.SizeOf(type); @@ -258,7 +253,7 @@ public Result Read(Type type, UIntPtr address) // Read the bytes from the process memory var readResult = _osService.ReadProcessMemory(ProcessHandle, address, (ulong)size); if (!readResult.IsSuccess) - return new ReadFailureOnSystemRead(readResult.Error); + return readResult.Failure; // Convert the bytes into the target type // Special case for booleans, which do not work well with Marshal.PtrToStructure @@ -273,12 +268,12 @@ public Result Read(Type type, UIntPtr address) var pointer = handle.AddrOfPinnedObject(); object? structure = Marshal.PtrToStructure(pointer, type); if (structure == null) - return new ReadFailureOnConversionFailure(); + return new ConversionFailure(); return structure; } catch (Exception) { - return new ReadFailureOnConversionFailure(); + return new ConversionFailure(); } finally { @@ -323,17 +318,17 @@ public Result Read(Type type, UIntPtr address) /// Known string that the pointer points to. Very short strings consisting of only a /// few characters may lead to unaccurate results. Strings containing diacritics or non-Latin characters provide /// better results. - /// A result holding the potential string settings if they were successfully determined, or a read failure + /// A result holding the potential string settings if they were successfully determined, or a failure /// otherwise. - public Result FindStringSettings(PointerPath pointerPath, + public Result FindStringSettings(PointerPath pointerPath, string expectedString) { if (!IsAttached) - return new FindStringSettingsFailureOnDetachedProcess(); + return new DetachedProcessFailure(); var addressResult = EvaluateMemoryAddress(pointerPath); - return addressResult.IsSuccess ? FindStringSettings(addressResult.Value, expectedString) - : new FindStringSettingsFailureOnPointerPathEvaluation(addressResult.Error); + return addressResult.IsSuccess ? FindStringSettings(addressResult.Value, expectedString) + : addressResult.Failure; } /// @@ -345,19 +340,19 @@ public Result FindStringSettings(Poin /// Known string that the pointer points to. Very short strings consisting of only a /// few characters may lead to unaccurate results. Strings containing diacritics or non-Latin characters provide /// better results. - /// A result holding the potential string settings if they were successfully determined, or a read failure + /// A result holding the potential string settings if they were successfully determined, or a failure /// otherwise. - public Result FindStringSettings(UIntPtr stringPointerAddress, + public Result FindStringSettings(UIntPtr stringPointerAddress, string expectedString) { if (!IsAttached) - return new FindStringSettingsFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (stringPointerAddress == UIntPtr.Zero) - return new FindStringSettingsFailureOnZeroPointer(); + return new ZeroPointerFailure(); if (!IsBitnessCompatible(stringPointerAddress)) - return new FindStringSettingsFailureOnIncompatibleBitness(stringPointerAddress); + return new IncompatibleBitnessPointerFailure(stringPointerAddress); if (string.IsNullOrWhiteSpace(expectedString)) - return new FindStringSettingsFailureOnNoSettingsFound(); + return new InvalidArgumentFailure(nameof(expectedString), "The expected string cannot be null or empty."); // We have to determine 4 parameters: // - The encoding of the string @@ -372,11 +367,11 @@ public Result FindStringSettings(UInt // Read the address of the actual string from the pointer var pointerResult = Read(stringPointerAddress); if (pointerResult.IsFailure) - return new FindStringSettingsFailureOnPointerReadFailure(pointerResult.Error); + return pointerResult.Failure; var pointerValue = pointerResult.Value; if (pointerValue == UIntPtr.Zero) - return new FindStringSettingsFailureOnZeroPointer(); + return new ZeroPointerFailure(); // Read the max amount of bytes we might need to read the string from // This is computed from the sum of the longest considered type prefix, the longest considered length prefix, @@ -390,7 +385,7 @@ public Result FindStringSettings(UInt var bytesResult = ReadBytes(pointerValue, (ulong)maxBytesTotal); if (bytesResult.IsFailure) - return new FindStringSettingsFailureOnStringReadFailure(bytesResult.Error); + return bytesResult.Failure; byte[] bytes = bytesResult.Value; // Iterate through all possible setting combinations from the considered setting values @@ -442,7 +437,7 @@ public Result FindStringSettings(UInt } // If we reach this point, we could not find any settings that would allow us to read the expected string. - return new FindStringSettingsFailureOnNoSettingsFound(); + return new UndeterminedStringSettingsFailure(); } #endregion @@ -493,16 +488,16 @@ public Result FindStringSettings(UInt /// Boolean indicating if the string is null-terminated. If true, the string will be /// read until the first null character. If false, the string will be read up to the maximum length specified. /// - /// The string read from the process memory, or a read failure. - public Result ReadRawString(PointerPath pointerPath, Encoding encoding, + /// The string read from the process memory, or a failure. + public Result ReadRawString(PointerPath pointerPath, Encoding encoding, int? maxLength = null, bool isNullTerminated = true) { if (!IsAttached) - return new ReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); var addressResult = EvaluateMemoryAddress(pointerPath); return addressResult.IsSuccess ? ReadRawString(addressResult.Value, encoding, maxLength, isNullTerminated) - : new ReadFailureOnPointerPathEvaluation(addressResult.Error); + : addressResult.Failure; } /// @@ -521,18 +516,18 @@ public Result ReadRawString(PointerPath pointerPath, Encodi /// Boolean indicating if the string is null-terminated. If true, the string will be /// read until the first null character. If false, the string will be read up to the maximum length specified. /// - /// The string read from the process memory, or a read failure. - public Result ReadRawString(UIntPtr address, Encoding encoding, + /// The string read from the process memory, or a failure. + public Result ReadRawString(UIntPtr address, Encoding encoding, int? maxLength = null, bool isNullTerminated = true) { if (!IsAttached) - return new ReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (address == UIntPtr.Zero) - return new ReadFailureOnZeroPointer(); + return new ZeroPointerFailure(); if (!IsBitnessCompatible(address)) - return new ReadFailureOnIncompatibleBitness(address); + return new IncompatibleBitnessPointerFailure(address); if (maxLength is < 0) - return new ReadFailureOnInvalidArguments("The maximum length cannot be negative."); + return new InvalidArgumentFailure(nameof(maxLength), "The maximum length cannot be negative."); if (maxLength == 0) return string.Empty; @@ -548,7 +543,7 @@ public Result ReadRawString(UIntPtr address, Encoding encod var buffer = new byte[byteSizeToRead]; var readResult = ReadBytesPartial(address, buffer, (ulong)byteSizeToRead); if (readResult.IsFailure) - return readResult.Error; + return readResult.Failure; // Check the number of bytes read ulong readByteCount = readResult.Value; @@ -574,15 +569,15 @@ public Result ReadRawString(UIntPtr address, Encoding encod /// Settings that define how to read the string. If you cannot figure out what settings to /// use, try to automatically determine the right settings for a /// known string pointer. See the documentation for more information. - /// The string read from the process memory, or a read failure. - public Result ReadStringPointer(PointerPath pointerPath, StringSettings settings) + /// The string read from the process memory, or a failure. + public Result ReadStringPointer(PointerPath pointerPath, StringSettings settings) { if (!IsAttached) - return new StringReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); var addressResult = EvaluateMemoryAddress(pointerPath); return addressResult.IsSuccess ? ReadStringPointer(addressResult.Value, settings) - : new StringReadFailureOnPointerPathEvaluation(addressResult.Error); + : addressResult.Failure; } /// @@ -593,24 +588,24 @@ public Result ReadStringPointer(PointerPath pointerPa /// Settings that define how to read the string. If you cannot figure out what settings to /// use, try to automatically determine the right settings for a /// known string pointer. See the documentation for more information. - /// The string read from the process memory, or a read failure. - public Result ReadStringPointer(UIntPtr address, StringSettings settings) + /// The string read from the process memory, or a failure. + public Result ReadStringPointer(UIntPtr address, StringSettings settings) { if (!IsAttached) - return new StringReadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (!IsBitnessCompatible(address)) - return new StringReadFailureOnIncompatibleBitness(address); + return new IncompatibleBitnessPointerFailure(address); if (address == UIntPtr.Zero) - return new StringReadFailureOnZeroPointer(); + return new ZeroPointerFailure(); if (!settings.IsValid) - return new StringReadFailureOnInvalidSettings(); + return new InvalidStringSettingsFailure(); // Start by reading the address of the string bytes var stringAddressResult = Read(address); if (stringAddressResult.IsFailure) - return new StringReadFailureOnPointerReadFailure(stringAddressResult.Error); + return stringAddressResult.Failure; if (stringAddressResult.Value == UIntPtr.Zero) - return new StringReadFailureOnZeroPointer(); + return new ZeroPointerFailure(); var stringAddress = stringAddressResult.Value; // The string settings will either have a null terminator or a length prefix. @@ -637,9 +632,8 @@ public Result ReadStringPointer(UIntPtr address, Stri /// /// Address of the string in the process memory. /// Settings that define how to read the string. - /// The string read from the process memory, or a read failure. - private Result ReadStringWithLengthPrefixInBytes(UIntPtr address, - StringSettings settings) + /// The string read from the process memory, or a failure. + private Result ReadStringWithLengthPrefixInBytes(UIntPtr address, StringSettings settings) { // The strategy when the string has a length prefix in bytes is the easiest one: // Read the length prefix, then we know exactly how many bytes to read, and then use the encoding to decode @@ -650,20 +644,20 @@ private Result ReadStringWithLengthPrefixInBytes(UInt var lengthPrefixAddress = (UIntPtr)(address.ToUInt64() + (ulong)lengthPrefixOffset); var lengthPrefixBytesResult = ReadBytes(lengthPrefixAddress, settings.LengthPrefix!.Size); if (lengthPrefixBytesResult.IsFailure) - return new StringReadFailureOnStringBytesReadFailure(lengthPrefixAddress, lengthPrefixBytesResult.Error); + return lengthPrefixBytesResult.Failure; ulong length = lengthPrefixBytesResult.Value.ReadUnsignedNumber(); if (length == 0) return string.Empty; if (length > (ulong)settings.MaxLength) - return new StringReadFailureOnStringTooLong(length); + return new StringTooLongFailure(length); // Read the string bytes (after the prefixes) int stringBytesOffset = lengthPrefixOffset + settings.LengthPrefix.Size; var stringBytesAddress = (UIntPtr)(address.ToUInt64() + (ulong)stringBytesOffset); var stringBytesResult = ReadBytes(stringBytesAddress, length); if (stringBytesResult.IsFailure) - return new StringReadFailureOnStringBytesReadFailure(stringBytesAddress, lengthPrefixBytesResult.Error); + return stringBytesResult.Failure; var stringBytes = stringBytesResult.Value; // Decode the bytes into a string using the encoding specified in the settings @@ -676,9 +670,8 @@ private Result ReadStringWithLengthPrefixInBytes(UInt /// /// Address of the string in the process memory. /// Settings that define how to read the string. - /// The string read from the process memory, or a read failure. - private Result ReadStringWithLengthPrefixInCharacters(UIntPtr address, - StringSettings settings) + /// The string read from the process memory, or a failure. + private Result ReadStringWithLengthPrefixInCharacters(UIntPtr address, StringSettings settings) { // The strategy when the string has a length prefix in characters is the following: // Read the length prefix, then get a stream that reads bytes from the process memory, and use a stream reader @@ -689,13 +682,13 @@ private Result ReadStringWithLengthPrefixInCharacters var lengthPrefixAddress = (UIntPtr)(address.ToUInt64() + (ulong)lengthPrefixOffset); var lengthPrefixBytesResult = ReadBytes(lengthPrefixAddress, settings.LengthPrefix!.Size); if (lengthPrefixBytesResult.IsFailure) - return new StringReadFailureOnStringBytesReadFailure(lengthPrefixAddress, lengthPrefixBytesResult.Error); + return lengthPrefixBytesResult.Failure; ulong expectedStringLength = lengthPrefixBytesResult.Value.ReadUnsignedNumber(); if (expectedStringLength == 0) return string.Empty; if (expectedStringLength > (ulong)settings.MaxLength) - return new StringReadFailureOnStringTooLong(expectedStringLength); + return new StringTooLongFailure(expectedStringLength); // Get a memory stream after the prefixes int stringBytesOffset = lengthPrefixOffset + settings.LengthPrefix.Size; @@ -727,8 +720,8 @@ private Result ReadStringWithLengthPrefixInCharacters /// /// Address of the string in the process memory. /// Settings that define how to read the string. - /// The string read from the process memory, or a read failure. - private Result ReadStringWithNullTerminator(UIntPtr address, StringSettings settings) + /// The string read from the process memory, or a failure. + private Result ReadStringWithNullTerminator(UIntPtr address, StringSettings settings) { // The strategy when the string has no length prefix but has a null-terminator is the following: // Get a stream that reads bytes from the process memory, and use a stream reader to read characters until we @@ -752,7 +745,7 @@ private Result ReadStringWithNullTerminator(UIntPtr a stringBuilder.Append((char)nextChar); if (stringBuilder.Length > settings.MaxLength) - return new StringReadFailureOnStringTooLong(null); + return new StringTooLongFailure(null); } return stringBuilder.ToString(); diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Threading.cs b/src/MindControl/ProcessMemory/ProcessMemory.Threading.cs index d752f1f..df5646f 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Threading.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Threading.cs @@ -16,19 +16,19 @@ public partial class ProcessMemory /// Address of the function (starting instruction) to run in the target process. /// /// An optional parameter to pass to the function. It can be either an address or a value. - /// This input value will be stored in register RDX for x64, or EBX for x86. If this does not match the call + /// This input value will be stored in register RCX for x64, or EBX for x86. If this does not match the call /// conventions of the target function, the thread must execute a "trampoline" code that arranges the parameter in /// the expected way before calling the function. See the documentation for more info. /// A result holding either the thread instance that you can use to wait for the thread to return, or a - /// error. - public DisposableResult RunThread(UIntPtr functionAddress, UIntPtr? parameter = null) + /// failure. + public DisposableResult RunThread(UIntPtr functionAddress, UIntPtr? parameter = null) { if (!IsAttached) - return new ThreadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); var startResult = StartThread(functionAddress, parameter ?? UIntPtr.Zero); if (startResult.IsFailure) - return startResult.Error; + return startResult.Failure; return new RemoteThread(_osService, startResult.Value); } @@ -40,20 +40,20 @@ public DisposableResult RunThread(UIntPtr functionA /// Pointer path to the function (or start instruction) to run in the target /// process. /// An optional parameter to pass to the function. It can be either an address or a value. - /// This input value will be stored in register RDX for x64, or EBX for x86. If this does not match the call + /// This input value will be stored in register RCX for x64, or EBX for x86. If this does not match the call /// conventions of the target function, the thread must execute a "trampoline" code that arranges the parameter in /// the expected way before calling the function. See the documentation for more info. /// A result holding either the thread instance that you can use to wait for the thread to return, or a - /// error. - public DisposableResult RunThread(PointerPath functionPointerPath, + /// failure. + public DisposableResult RunThread(PointerPath functionPointerPath, UIntPtr? parameter = null) { if (!IsAttached) - return new ThreadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); var evaluateResult = EvaluateMemoryAddress(functionPointerPath); if (evaluateResult.IsFailure) - return new ThreadFailureOnPointerPathEvaluation(evaluateResult.Error); + return evaluateResult.Failure; return RunThread(evaluateResult.Value, parameter); } @@ -66,20 +66,20 @@ public DisposableResult RunThread(PointerPath funct /// Name of the module containing the function to run (e.g. "kernel32.dll"). /// Name of the exported function to run from the specified module. /// An optional parameter to pass to the function. It can be either an address or a value. - /// This input value will be stored in register RDX for x64, or EBX for x86. If this does not match the call + /// This input value will be stored in register RCX for x64, or EBX for x86. If this does not match the call /// conventions of the target function, the thread must execute a "trampoline" code that arranges the parameter in /// the expected way before calling the function. See the documentation for more info. /// A result holding either the thread instance that you can use to wait for the thread to return, or a - /// error. - public DisposableResult RunThread(string moduleName, string functionName, + /// failure. + public DisposableResult RunThread(string moduleName, string functionName, UIntPtr? parameter = null) { if (!IsAttached) - return new ThreadFailureOnDetachedProcess(); + return new DetachedProcessFailure(); var functionAddressResult = FindFunctionAddress(moduleName, functionName); if (functionAddressResult.IsFailure) - return functionAddressResult.Error; + return functionAddressResult.Failure; return RunThread(functionAddressResult.Value, parameter); } @@ -95,22 +95,17 @@ public DisposableResult RunThread(string moduleName /// /// Parameter to pass to the function. Use if the function does /// not take parameters. - /// A result holding either the handle to the thread, or a error. - private Result StartThread(UIntPtr functionAddress, UIntPtr parameter) + /// A result holding either the handle to the thread, or a failure. + private Result StartThread(UIntPtr functionAddress, UIntPtr parameter) { if (functionAddress == UIntPtr.Zero) - return new ThreadFailureOnInvalidArguments("The function address cannot be zero."); + return new InvalidArgumentFailure(nameof(functionAddress), "The function address cannot be zero."); if (!Is64Bit && functionAddress.ToUInt64() > uint.MaxValue) - return new ThreadFailureOnInvalidArguments( - $"The function address exceeds the maximum value for 32-bit processes ({uint.MaxValue})."); + return new IncompatibleBitnessPointerFailure(functionAddress); if (!Is64Bit && parameter.ToUInt64() > uint.MaxValue) - return new ThreadFailureOnInvalidArguments( - $"The provided parameter exceeds the maximum value for 32-bit processes ({uint.MaxValue})."); + return new IncompatibleBitnessPointerFailure(parameter); - var remoteThreadResult = _osService.CreateRemoteThread(ProcessHandle, functionAddress, parameter); - if (remoteThreadResult.IsFailure) - return new ThreadFailureOnSystemFailure("Failed to create the thread.", remoteThreadResult.Error); - return remoteThreadResult.Value; + return _osService.CreateRemoteThread(ProcessHandle, functionAddress, parameter); } /// @@ -118,18 +113,18 @@ private Result StartThread(UIntPtr functionAddress, UIntP /// /// Name of the module containing the function to find. /// Name of the function to find in the export table of the module. - /// A result holding either the address of the function, or a error. - private Result FindFunctionAddress(string moduleName, string functionName) + /// A result holding either the address of the function, or a failure. + private Result FindFunctionAddress(string moduleName, string functionName) { var module = GetModule(moduleName); if (module == null) - return new ThreadFailureOnFunctionNotFound($"The module \"{moduleName}\" is not loaded in the process."); + return new FunctionNotFoundFailure($"The module \"{moduleName}\" is not loaded in the process."); var exportTable = module.ReadExportTable(); if (exportTable.IsFailure) - return new ThreadFailureOnFunctionNotFound( - $"Failed to read the export table of the module \"{moduleName}\": {exportTable.Error}."); + return new FunctionNotFoundFailure( + $"Failed to read the export table of the module \"{moduleName}\": {exportTable.Failure}."); if (!exportTable.Value.TryGetValue(functionName, out UIntPtr functionAddress)) - return new ThreadFailureOnFunctionNotFound( + return new FunctionNotFoundFailure( $"The function \"{functionName}\" was not found in the export table of the module \"{moduleName}\"."); return functionAddress; } diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs index 4b868c6..7f4ce9f 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs @@ -16,16 +16,16 @@ public partial class ProcessMemory /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - public Result WriteBytes(PointerPath path, byte[] value, + /// A result indicating either a success or a failure. + public Result WriteBytes(PointerPath path, byte[] value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { if (!IsAttached) - return new WriteFailureOnDetachedProcess(); + return new DetachedProcessFailure(); var addressResult = EvaluateMemoryAddress(path); return addressResult.IsSuccess ? WriteBytes(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + : addressResult.Failure; } /// @@ -35,16 +35,16 @@ public Result WriteBytes(PointerPath path, byte[] value, /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - public Result WriteBytes(UIntPtr address, Span value, + /// A result indicating either a success or a failure. + public Result WriteBytes(UIntPtr address, Span value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { if (!IsAttached) - return new WriteFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (address == UIntPtr.Zero) - return new WriteFailureOnZeroPointer(); + return new ZeroPointerFailure(); if (!IsBitnessCompatible(address)) - return new WriteFailureOnIncompatibleBitness(address); + return new IncompatibleBitnessPointerFailure(address); // Remove protection if needed memoryProtectionStrategy ??= DefaultWriteStrategy; @@ -55,7 +55,7 @@ public Result WriteBytes(UIntPtr address, Span value, address, MemoryProtection.ExecuteReadWrite); if (protectionRemovalResult.IsFailure) - return new WriteFailureOnSystemProtectionRemoval(address, protectionRemovalResult.Error); + return new MemoryProtectionRemovalFailure(address, protectionRemovalResult.Failure); previousProtection = protectionRemovalResult.Value; } @@ -63,7 +63,7 @@ public Result WriteBytes(UIntPtr address, Span value, // Write memory var writeResult = _osService.WriteProcessMemory(ProcessHandle, address, value); if (writeResult.IsFailure) - return new WriteFailureOnSystemWrite(address, writeResult.Error); + return writeResult; // Restore protection if needed if (memoryProtectionStrategy == MemoryProtectionStrategy.RemoveAndRestore @@ -73,10 +73,10 @@ public Result WriteBytes(UIntPtr address, Span value, address, previousProtection!.Value); if (protectionRestorationResult.IsFailure) - return new WriteFailureOnSystemProtectionRestoration(address, protectionRestorationResult.Error); + return new MemoryProtectionRestorationFailure(address, protectionRestorationResult.Failure); } - return Result.Success; + return Result.Success; } #endregion @@ -92,15 +92,15 @@ public Result WriteBytes(UIntPtr address, Span value, /// of this instance is used. /// Type of the value to write. /// Thrown when the type of the value is not supported. - /// A successful result, or a write failure - public Result Write(PointerPath path, T value, + /// A result indicating either a success or a failure. + public Result Write(PointerPath path, T value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { if (!IsAttached) - return new WriteFailureOnDetachedProcess(); + return new DetachedProcessFailure(); var addressResult = EvaluateMemoryAddress(path); return addressResult.IsSuccess ? Write(addressResult.Value, value, memoryProtectionStrategy) - : new WriteFailureOnPointerPathEvaluation(addressResult.Error); + : addressResult.Failure; } /// @@ -111,22 +111,22 @@ public Result Write(PointerPath path, T value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// Type of the value to write. - /// A successful result, or a write failure - public Result Write(UIntPtr address, T value, + /// A result indicating either a success or a failure. + public Result Write(UIntPtr address, T value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { if (!IsAttached) - return new WriteFailureOnDetachedProcess(); + return new DetachedProcessFailure(); if (address == UIntPtr.Zero) - return new WriteFailureOnZeroPointer(); + return new ZeroPointerFailure(); if (!IsBitnessCompatible(address)) - return new WriteFailureOnIncompatibleBitness(address); + return new IncompatibleBitnessPointerFailure(address); if (value == null) - return new WriteFailureOnInvalidArguments("The value to write cannot be null."); + return new InvalidArgumentFailure(nameof(value), "The value to write cannot be null."); if (value is IntPtr ptr && ptr.ToInt64() > uint.MaxValue) - return new WriteFailureOnIncompatibleBitness((UIntPtr)ptr); + return new IncompatibleBitnessPointerFailure((UIntPtr)ptr); if (value is UIntPtr uptr && uptr.ToUInt64() > uint.MaxValue) - return new WriteFailureOnIncompatibleBitness(uptr); + return new IncompatibleBitnessPointerFailure(uptr); return value switch { @@ -158,14 +158,14 @@ public Result Write(UIntPtr address, T value, /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. /// Type of the value to write. - /// A successful result, or a write failure - private Result ConvertAndWriteBytes(UIntPtr address, T value, + /// A result indicating either a success or a failure. + private Result ConvertAndWriteBytes(UIntPtr address, T value, MemoryProtectionStrategy? memoryProtectionStrategy = null) { if (value == null) - return new WriteFailureOnInvalidArguments("The value to write cannot be null."); + return new InvalidArgumentFailure(nameof(value), "The value to write cannot be null."); if (value is not ValueType) - return new WriteFailureOnUnsupportedType(value.GetType()); + return new UnsupportedTypeWriteFailure(value.GetType()); byte[] bytes; try @@ -174,7 +174,7 @@ private Result ConvertAndWriteBytes(UIntPtr address, T value, } catch (Exception e) { - return new WriteFailureOnConversion(typeof(T), e); + return new ConversionWriteFailure(typeof(T), e); } return WriteBytes(address, bytes, memoryProtectionStrategy); @@ -188,9 +188,8 @@ private Result ConvertAndWriteBytes(UIntPtr address, T value, /// as a byte with the value 0. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - private Result WriteBool(UIntPtr address, bool value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A result indicating either a success or a failure. + private Result WriteBool(UIntPtr address, bool value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, new[] { (byte)(value ? 1 : 0) }, memoryProtectionStrategy); /// @@ -200,9 +199,8 @@ private Result WriteBool(UIntPtr address, bool value, /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - private Result WriteByte(UIntPtr address, byte value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A result indicating either a success or a failure. + private Result WriteByte(UIntPtr address, byte value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, new[] { value }, memoryProtectionStrategy); /// @@ -212,9 +210,8 @@ private Result WriteByte(UIntPtr address, byte value, /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - private Result WriteShort(UIntPtr address, short value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A result indicating either a success or a failure. + private Result WriteShort(UIntPtr address, short value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); /// @@ -224,9 +221,8 @@ private Result WriteShort(UIntPtr address, short value, /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - private Result WriteUShort(UIntPtr address, ushort value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A result indicating either a success or a failure. + private Result WriteUShort(UIntPtr address, ushort value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); /// @@ -236,9 +232,8 @@ private Result WriteUShort(UIntPtr address, ushort value, /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - private Result WriteInt(UIntPtr address, int value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A result indicating either a success or a failure. + private Result WriteInt(UIntPtr address, int value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); /// @@ -248,9 +243,8 @@ private Result WriteInt(UIntPtr address, int value, /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - private Result WriteUInt(UIntPtr address, uint value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A result indicating either a success or a failure. + private Result WriteUInt(UIntPtr address, uint value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); /// @@ -260,9 +254,8 @@ private Result WriteUInt(UIntPtr address, uint value, /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - private Result WriteIntPtr(UIntPtr address, IntPtr value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A result indicating either a success or a failure. + private Result WriteIntPtr(UIntPtr address, IntPtr value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, value.ToBytes(Is64Bit), memoryProtectionStrategy); /// @@ -272,9 +265,8 @@ private Result WriteIntPtr(UIntPtr address, IntPtr value, /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - private Result WriteFloat(UIntPtr address, float value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A result indicating either a success or a failure. + private Result WriteFloat(UIntPtr address, float value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); /// @@ -284,9 +276,8 @@ private Result WriteFloat(UIntPtr address, float value, /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - private Result WriteLong(UIntPtr address, long value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A result indicating either a success or a failure. + private Result WriteLong(UIntPtr address, long value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); /// @@ -296,9 +287,8 @@ private Result WriteLong(UIntPtr address, long value, /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - private Result WriteULong(UIntPtr address, ulong value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A result indicating either a success or a failure. + private Result WriteULong(UIntPtr address, ulong value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); /// @@ -308,9 +298,8 @@ private Result WriteULong(UIntPtr address, ulong value, /// Value to write. /// Strategy to use to deal with memory protection. If null (default), the /// of this instance is used. - /// A successful result, or a write failure - private Result WriteDouble(UIntPtr address, double value, - MemoryProtectionStrategy? memoryProtectionStrategy = null) + /// A result indicating either a success or a failure. + private Result WriteDouble(UIntPtr address, double value, MemoryProtectionStrategy? memoryProtectionStrategy = null) => WriteBytes(address, BitConverter.GetBytes(value), memoryProtectionStrategy); #endregion diff --git a/src/MindControl/ProcessMemory/ProcessMemory.cs b/src/MindControl/ProcessMemory/ProcessMemory.cs index f6b6bb4..d3dce8b 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.cs @@ -47,23 +47,23 @@ public partial class ProcessMemory : IDisposable /// /// Attaches to a process with the given name and returns the resulting instance. - /// If multiple processes with the specified name are running, a - /// will be returned. + /// If multiple processes with the specified name are running, a will + /// be returned. /// When there is any risk of this happening, it is recommended to use instead. /// /// Name of the process to open. - /// A result holding either the attached process instance, or an error. - public static Result OpenProcessByName(string processName) + /// A result holding either the attached process instance, or a failure. + public static Result OpenProcessByName(string processName) { var matches = Process.GetProcessesByName(processName); if (matches.Length == 0) - return new AttachFailureOnTargetProcessNotFound(); + return new TargetProcessNotFoundFailure(); if (matches.Length > 1) { var pids = matches.Select(p => p.Id).ToArray(); foreach (var process in matches) process.Dispose(); - return new AttachFailureOnMultipleTargetProcessesFound(pids); + return new MultipleTargetProcessesFailure(pids); } return OpenProcess(matches.First(), true, new Win32Service()); @@ -74,8 +74,8 @@ public static Result OpenProcessByName(string proc /// instance. /// /// Identifier of the process to attach to. - /// A result holding either the attached process instance, or an error. - public static Result OpenProcessById(int pid) + /// A result holding either the attached process instance, or a failure. + public static Result OpenProcessById(int pid) { try { @@ -85,7 +85,7 @@ public static Result OpenProcessById(int pid) catch (ArgumentException) { // Process.GetProcessById throws an ArgumentException when the PID does not match any process. - return new AttachFailureOnTargetProcessNotFound(); + return new TargetProcessNotFoundFailure(); } } @@ -93,8 +93,8 @@ public static Result OpenProcessById(int pid) /// Attaches to the given process, and returns the resulting instance. /// /// Process to attach to. - /// A result holding either the attached process instance, or an error. - public static Result OpenProcess(Process target) + /// A result holding either the attached process instance, or a failure. + public static Result OpenProcess(Process target) => OpenProcess(target, false, new Win32Service()); /// @@ -104,8 +104,8 @@ public static Result OpenProcess(Process target) /// /// Process to attach to. /// Service that provides system-specific process-oriented features. - /// A result holding either the attached process instance, or an error. - public static Result OpenProcess(Process target, IOperatingSystemService osService) + /// A result holding either the attached process instance, or a failure. + public static Result OpenProcess(Process target, IOperatingSystemService osService) => OpenProcess(target, false, osService); /// @@ -115,25 +115,25 @@ public static Result OpenProcess(Process target, I /// Indicates if this instance should take ownership of the /// , meaning it has the responsibility to dispose it. /// Service that provides system-specific process-oriented features. - /// A result holding either the attached process instance, or an error. - internal static Result OpenProcess(Process target, bool ownsProcessInstance, + /// A result holding either the attached process instance, or a failure. + internal static Result OpenProcess(Process target, bool ownsProcessInstance, IOperatingSystemService osService) { if (target.HasExited) - return new AttachFailureOnTargetProcessNotRunning(); + return new TargetProcessNotRunningFailure(); // Determine target bitness var is64BitResult = osService.IsProcess64Bit(target.Id); if (is64BitResult.IsFailure) - return new AttachFailureOnSystemError(is64BitResult.Error); + return is64BitResult.Failure; var is64Bit = is64BitResult.Value; if (is64Bit && IntPtr.Size != 8) - return new AttachFailureOnIncompatibleBitness(); + return new IncompatibleBitnessProcessFailure(); // Open the process with the required access flags var openResult = osService.OpenProcess(target.Id); if (openResult.IsFailure) - return new AttachFailureOnSystemError(openResult.Error); + return openResult.Failure; var processHandle = openResult.Value; // Build the instance with the handle and bitness information @@ -178,8 +178,19 @@ private ProcessMemory(Process process, bool ownsProcessInstance, IntPtr processH /// Gets a new instance of representing the attached process. /// The returned instance is owned by the caller and should be disposed when no longer needed. /// - public Process GetAttachedProcessInstance() => Process.GetProcessById(_process.Id); - + public DisposableResult GetAttachedProcessInstance() + { + try + { + return Process.GetProcessById(_process.Id); + } + catch (Exception) + { + // The process is no longer running + return new TargetProcessNotRunningFailure(); + } + } + #region Dispose /// diff --git a/src/MindControl/ProcessTracker.cs b/src/MindControl/ProcessTracker.cs index 2fd752e..71ea85c 100644 --- a/src/MindControl/ProcessTracker.cs +++ b/src/MindControl/ProcessTracker.cs @@ -109,7 +109,7 @@ private void Detach() { var process = GetTargetProcess(); return process == null ? null : - ProcessMemory.OpenProcess(process, true, new Win32Service()).GetValueOrDefault(); + ProcessMemory.OpenProcess(process, true, new Win32Service()).ValueOrDefault(); } /// diff --git a/src/MindControl/Results/AllocationFailure.cs b/src/MindControl/Results/AllocationFailure.cs index 341f73a..3d8b980 100644 --- a/src/MindControl/Results/AllocationFailure.cs +++ b/src/MindControl/Results/AllocationFailure.cs @@ -1,36 +1,15 @@ namespace MindControl.Results; -/// Represents a failure in a memory allocation operation. -public abstract record AllocationFailure; - -/// Represents a failure in a memory allocation operation when the target process is not attached. -public record AllocationFailureOnDetachedProcess : AllocationFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => Failure.DetachedErrorMessage; -} - -/// Represents a failure in a memory allocation operation when the provided arguments are invalid. -/// Message that describes how the arguments fail to meet expectations. -public record AllocationFailureOnInvalidArguments(string Message) : AllocationFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The arguments provided are invalid: {Message}"; -} - /// /// Represents a failure in a memory allocation operation when the provided limit range is not within the bounds of the /// target process applicative memory range. /// /// Applicative memory range of the target process. -public record AllocationFailureOnLimitRangeOutOfBounds(MemoryRange ApplicativeMemoryRange) : AllocationFailure +public record LimitRangeOutOfBoundsFailure(MemoryRange ApplicativeMemoryRange) + : Failure($"The provided limit range is not within the bounds of the target process applicative memory range ({ApplicativeMemoryRange}).") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"The provided limit range is not within the bounds of the target process applicative memory range ({ApplicativeMemoryRange})."; + /// Applicative memory range of the target process. + public MemoryRange ApplicativeMemoryRange { get; init; } = ApplicativeMemoryRange; } /// @@ -39,11 +18,9 @@ public override string ToString() /// /// Searched range in the target process. /// Last memory region address searched in the target process. -public record AllocationFailureOnNoFreeMemoryFound(MemoryRange SearchedRange, UIntPtr LastRegionAddressSearched) - : AllocationFailure +public record NoFreeMemoryFailure(MemoryRange SearchedRange, UIntPtr LastRegionAddressSearched) + : Failure($"No free memory range large enough to accomodate the specified size was found in the target process within the searched range ({SearchedRange}).") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"No free memory range large enough to accomodate the specified size was found in the target process within the searched range ({SearchedRange})."; + /// Searched range in the target process. + public MemoryRange SearchedRange { get; init; } = SearchedRange; } diff --git a/src/MindControl/Results/AttachFailure.cs b/src/MindControl/Results/AttachFailure.cs index 669776a..24d0888 100644 --- a/src/MindControl/Results/AttachFailure.cs +++ b/src/MindControl/Results/AttachFailure.cs @@ -1,53 +1,21 @@ namespace MindControl.Results; -/// Represents a failure in a process attach operation. -public abstract record AttachFailure; - /// Represents a failure in a process attach operation when the target process could not be found. -public record AttachFailureOnTargetProcessNotFound : AttachFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => "The target process could not be found. Make sure the process is running."; -} +public record TargetProcessNotFoundFailure() + : Failure("The target process could not be found. Make sure the process is running."); /// /// Represents a failure in a process attach operation when multiple processes with the given name were found. /// /// Identifiers of running processes with the given name. -public record AttachFailureOnMultipleTargetProcessesFound(int[] Pids) : AttachFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => "Multiple processes with the given name were found. Use a PID to disambiguate."; -} +public record MultipleTargetProcessesFailure(int[] Pids) + : Failure("Multiple processes with the given name were found. Use a PID to disambiguate."); /// Represents a failure in a process attach operation when the target process is not running. -public record AttachFailureOnTargetProcessNotRunning : AttachFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => "The target process is not running."; -} +public record TargetProcessNotRunningFailure() : Failure("The target process is not running."); /// /// Represents a failure in a process attach operation when the target process is 64-bit and the current process is /// 32-bit. /// -public record AttachFailureOnIncompatibleBitness : AttachFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => "Cannot open a 64-bit process from a 32-bit process."; -} - -/// Represents a failure in a process attach operation when a system error occurs. -/// Details about the system error that occurred. -public record AttachFailureOnSystemError(SystemFailure Details) : AttachFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"A system error occurred while attempting to attach to the process: {Details}"; -} \ No newline at end of file +public record IncompatibleBitnessProcessFailure() : Failure("Cannot attach to a 64-bit process from a 32-bit process."); \ No newline at end of file diff --git a/src/MindControl/Results/Failure.cs b/src/MindControl/Results/Failure.cs index 8fa677b..d76817e 100644 --- a/src/MindControl/Results/Failure.cs +++ b/src/MindControl/Results/Failure.cs @@ -1,11 +1,30 @@ namespace MindControl.Results; /// -/// Provides members to be used in error results. +/// Base class for failures. Can also be used directly when no specific failure type is needed. /// -public static class Failure +/// Message describing the failure. +public record Failure(string Message) { - /// Message used in error results when the process is detached. - public const string DetachedErrorMessage = - "The process is not attached. It may have exited or the process memory instance may have been disposed."; -} \ No newline at end of file + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() => Message; +} + +/// Failure that occurs when the process is not attached. +public record DetachedProcessFailure() + : Failure("The process is not attached. It may have exited or the process memory instance may have been disposed."); + +/// Represents a failure in an operating system operation when the provided arguments are invalid. +/// Name of the argument that caused the failure. +/// Message that describes how the argument fails to meet expectations. +public record InvalidArgumentFailure(string ArgumentName, string Message) + : Failure($"The value provided for \"{ArgumentName}\" is invalid: {Message}") +{ + /// Name of the argument that caused the failure. + public string ArgumentName { get; init; } = ArgumentName; +} + +/// Represents a failure when an operation is not supported. +/// Message that describes why the operation is not supported. +public record NotSupportedFailure(string Message) : Failure($"This operation is not supported: {Message}"); diff --git a/src/MindControl/Results/FindBytesFailure.cs b/src/MindControl/Results/FindBytesFailure.cs index f984e09..b5b9f13 100644 --- a/src/MindControl/Results/FindBytesFailure.cs +++ b/src/MindControl/Results/FindBytesFailure.cs @@ -3,9 +3,4 @@ /// Represents a failure occurring when the provided byte search pattern is invalid. /// Message that explains what makes the pattern invalid. public record InvalidBytePatternFailure(string Message) -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"The provided byte search pattern is invalid: {Message}"; -} + : Failure($"The provided byte search pattern is invalid: {Message}"); diff --git a/src/MindControl/Results/FindStringSettingsFailure.cs b/src/MindControl/Results/FindStringSettingsFailure.cs deleted file mode 100644 index dad671f..0000000 --- a/src/MindControl/Results/FindStringSettingsFailure.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace MindControl.Results; - -/// Represents a failure in a string settings search operation. -public abstract record FindStringSettingsFailure; - -/// -/// Represents a failure in a string settings search operation when the target process is not attached. -/// -public record FindStringSettingsFailureOnDetachedProcess : FindStringSettingsFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => Failure.DetachedErrorMessage; -} - -/// -/// Represents a failure in a string settings search operation when failing to evaluate the specified pointer path. -/// -/// Underlying path evaluation failure details. -public record FindStringSettingsFailureOnPointerPathEvaluation(PathEvaluationFailure Details) - : FindStringSettingsFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"Failed to evaluate the specified pointer path: {Details}"; -} - -/// -/// Represents a failure in a string settings search operation when the target process is 32-bit, but the target memory -/// address is not within the 32-bit address space. -/// -/// Address that caused the failure. -public record FindStringSettingsFailureOnIncompatibleBitness(UIntPtr Address) : FindStringSettingsFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"The address to read, {Address}, is a 64-bit address, but the target process is 32-bit."; -} - -/// -/// Represents a failure in a string settings search operation when failing to read the value of the given pointer. -/// -/// Underlying read failure details. -public record FindStringSettingsFailureOnPointerReadFailure(ReadFailure Details) : FindStringSettingsFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"Failed to read a pointer while searching for string settings: {Details}"; -} - -/// -/// Represents a failure in a string settings search operation when the given pointer is a zero pointer. -/// -public record FindStringSettingsFailureOnZeroPointer : FindStringSettingsFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => "The given pointer is a zero pointer."; -} - -/// -/// Represents a failure in a string settings search operation when failing to read bytes at the address pointed by the -/// given pointer. -/// -/// Underlying read failure details. -public record FindStringSettingsFailureOnStringReadFailure(ReadFailure Details) : FindStringSettingsFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"Failed to read bytes at the address pointed by the given pointer while searching for string settings: {Details}"; -} - -/// -/// Represents a failure in a string settings search operation when no adequate settings were found to read the given -/// string from the specified pointer. -/// -public record FindStringSettingsFailureOnNoSettingsFound : FindStringSettingsFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => "No adequate settings were found to read the given string from the specified pointer."; -} diff --git a/src/MindControl/Results/InjectionFailure.cs b/src/MindControl/Results/InjectionFailure.cs index 26edac6..fb0ea59 100644 --- a/src/MindControl/Results/InjectionFailure.cs +++ b/src/MindControl/Results/InjectionFailure.cs @@ -1,65 +1,22 @@ namespace MindControl.Results; -/// Represents a failure in a library injection operation. -public abstract record InjectionFailure; - -/// Represents a failure in a library injection operation when the target process is not attached. -public record InjectionFailureOnDetachedProcess : InjectionFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => Failure.DetachedErrorMessage; -} - /// /// Represents a failure in a library injection operation when the library file to inject was not found. /// /// Path to the library file that was not found. -public record InjectionFailureOnLibraryFileNotFound(string LibraryPath) : InjectionFailure +public record LibraryFileNotFoundFailure(string LibraryPath) + : Failure($"The library file to inject was not found at \"{LibraryPath}\".") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"The library file to inject was not found at \"{LibraryPath}\"."; + /// Path to the library file that was not found. + public string LibraryPath { get; init; } = LibraryPath; } /// /// Represents a failure in a library injection operation when the module to inject is already loaded in the target /// process. /// -public record InjectionFailureOnModuleAlreadyLoaded : InjectionFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => "The module to inject is already loaded in the target process."; -} - -/// Represents a failure in a library injection operation when trying to store function parameters. -/// Details about the failure. -public record InjectionFailureOnParameterStorage(StoreFailure Details) : InjectionFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"Failed to store function parameters required to inject the library: {Details}"; -} - -/// Represents a failure in a library injection operation when running the library loading thread. -/// Details about the failure. -public record InjectionFailureOnThreadFailure(ThreadFailure Details) : InjectionFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"Failed to inject the library due to a remote thread failure: {Details}"; -} +public record ModuleAlreadyLoadedFailure() : Failure("The module to inject is already loaded in the target process."); /// Represents a failure in a library injection operation when the library function call fails. -public record InjectionFailureOnLoadLibraryFailure : InjectionFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => "The LoadLibraryW function returned an exit code of 0, indicating failure. Check that the library is valid and compatible with the target process (32-bit vs 64-bit)."; -} +public record LibraryLoadFailure() + : Failure("The LoadLibraryW function returned an exit code of 0, indicating failure. Check that the library is valid and compatible with the target process (32-bit vs 64-bit)."); diff --git a/src/MindControl/Results/NotSupportedFailure.cs b/src/MindControl/Results/NotSupportedFailure.cs deleted file mode 100644 index 979e17f..0000000 --- a/src/MindControl/Results/NotSupportedFailure.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace MindControl.Results; - -/// Represents a failure when an operation is not supported. -/// Message that describes why the operation is not supported. -public record NotSupportedFailure(string Message) -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - { - return $"This operation is not supported: {Message}"; - } -} \ No newline at end of file diff --git a/src/MindControl/Results/PathEvaluationFailure.cs b/src/MindControl/Results/PathEvaluationFailure.cs index ec4d958..65e35d4 100644 --- a/src/MindControl/Results/PathEvaluationFailure.cs +++ b/src/MindControl/Results/PathEvaluationFailure.cs @@ -1,41 +1,24 @@ namespace MindControl.Results; -/// Represents a failure in a path evaluation operation. -public abstract record PathEvaluationFailure; - -/// Represents a failure in a path evaluation operation when the target process is not attached. -public record PathEvaluationFailureOnDetachedProcess : PathEvaluationFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => Failure.DetachedErrorMessage; -} - /// /// Represents a failure in a path evaluation operation when the target process is 32-bit, but the target memory /// address is not within the 32-bit address space. /// /// Address where the value causing the issue was read. May be null if the first address /// in the path caused the failure. -public record PathEvaluationFailureOnIncompatibleBitness(UIntPtr? PreviousAddress = null) : PathEvaluationFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => "The specified pointer path contains 64-bit offsets, but the target process is 32-bit."; -} +public record IncompatiblePointerPathBitnessFailure(UIntPtr? PreviousAddress = null) + : Failure("The specified pointer path contains 64-bit offsets, but the target process is 32-bit. Check the offsets in the path."); /// /// Represents a failure in a path evaluation operation when the base module specified in the pointer path was not /// found. /// /// Name of the module that was not found. -public record PathEvaluationFailureOnBaseModuleNotFound(string ModuleName) : PathEvaluationFailure +public record BaseModuleNotFoundFailure(string ModuleName) + : Failure($"The module \"{ModuleName}\", referenced in the pointer path, was not found in the target process.") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"The module \"{ModuleName}\", referenced in the pointer path, was not found in the target process."; + /// Name of the module that was not found. + public string ModuleName { get; init; } = ModuleName; } /// @@ -45,24 +28,11 @@ public override string ToString() /// Address that triggered the failure after the offset. May be null if the first address /// in the path caused the failure. /// Offset that caused the failure. -public record PathEvaluationFailureOnPointerOutOfRange(UIntPtr? PreviousAddress, PointerOffset Offset) - : PathEvaluationFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => "The pointer path evaluated a pointer to an address that is out of the target process address space range."; -} +public record PointerOutOfRangeFailure(UIntPtr? PreviousAddress, PointerOffset Offset) + : Failure("The pointer path evaluated a pointer to an address that is out of the target process address space range."); /// -/// Represents a failure in a path evaluation operation when invoking the system API to read an address. +/// Represents a failure when a null pointer is accessed. /// -/// Address that caused the failure. -/// Details about the failure. -public record PathEvaluationFailureOnPointerReadFailure(UIntPtr Address, ReadFailure Details) : PathEvaluationFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"Failed to read a pointer at the address {Address}: {Details}"; -} \ No newline at end of file +public record ZeroPointerFailure() + : Failure("One of the pointers accessed as part of the operation was a null pointer."); diff --git a/src/MindControl/Results/ReadFailure.cs b/src/MindControl/Results/ReadFailure.cs index d4e4482..b8be573 100644 --- a/src/MindControl/Results/ReadFailure.cs +++ b/src/MindControl/Results/ReadFailure.cs @@ -1,96 +1,29 @@ namespace MindControl.Results; -/// Represents a failure in a memory read operation. -public abstract record ReadFailure; - -/// Represents a failure in a memory read operation when the target process is not attached. -public record ReadFailureOnDetachedProcess : ReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => Failure.DetachedErrorMessage; -} - -/// Represents a failure in a memory read operation when the arguments provided are invalid. -/// Message that describes how the arguments fail to meet expectations. -public record ReadFailureOnInvalidArguments(string Message) : ReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The arguments provided are invalid: {Message}"; -} - -/// Represents a failure in a memory read operation when resolving the address in the target process. -/// Details about the failure. -/// Type of the underlying failure. -public record ReadFailureOnAddressResolution(T Details) : ReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"Failed to resolve the address: {Details}"; -} - -/// -/// Represents a failure in a memory read operation when evaluating the pointer path to the target memory. -/// -/// Details about the failure. -public record ReadFailureOnPointerPathEvaluation(PathEvaluationFailure Details) : ReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"Failed to evaluate the specified pointer path: {Details}"; -} - /// /// Represents a failure in a memory read operation when the target process is 32-bit, but the target memory address is /// not within the 32-bit address space. /// /// Address that caused the failure. -public record ReadFailureOnIncompatibleBitness(UIntPtr Address) : ReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"The address to read, {Address}, is a 64-bit address, but the target process is 32-bit."; -} - -/// Represents a failure in a memory read operation when the target pointer is a zero pointer. -public record ReadFailureOnZeroPointer : ReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => "The address to read is a zero pointer."; -} - -/// -/// Represents a failure in a memory read operation when invoking the system API to read the target memory. -/// -/// Details about the failure. -public record ReadFailureOnSystemRead(SystemFailure Details) : ReadFailure +public record IncompatibleBitnessPointerFailure(UIntPtr Address) + : Failure($"The address to read, {Address}, is a 64-bit address, but the target process is 32-bit.") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"Failed to read at the target address: {Details}"; + /// Address that caused the failure. + public UIntPtr Address { get; init; } = Address; } /// /// Represents a failure in a memory read operation when trying to convert the bytes read from memory to the target /// type. /// -public record ReadFailureOnConversionFailure : ReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => "Failed to convert the bytes read from memory to the target type. Check that the type does not contain references or pointers."; -} +public record ConversionFailure() + : Failure("Failed to convert the bytes read from memory to the target type. Check that the target type does not contain reference types or pointers."); /// Represents a failure in a memory read operation when the type to read is not supported. /// Type that caused the failure. -public record ReadFailureOnUnsupportedType(Type ProvidedType) : ReadFailure +public record UnsupportedTypeReadFailure(Type ProvidedType) + : Failure($"The type {ProvidedType} is not supported. Reference types are not supported.") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"The type {ProvidedType} is not supported. Reference types are not supported."; + /// Type that caused the failure. + public Type ProvidedType { get; init; } = ProvidedType; } \ No newline at end of file diff --git a/src/MindControl/Results/ReservationFailure.cs b/src/MindControl/Results/ReservationFailure.cs index 42a32f9..f2ef363 100644 --- a/src/MindControl/Results/ReservationFailure.cs +++ b/src/MindControl/Results/ReservationFailure.cs @@ -1,37 +1,13 @@ namespace MindControl.Results; -/// Represents a failure in a memory reservation operation. -public abstract record ReservationFailure; - /// /// Represents a failure in a memory reservation operation when the target allocation has been disposed. /// -public record ReservationFailureOnDisposedAllocation : ReservationFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => "The target allocation has been disposed."; -} - -/// -/// Represents a failure in a memory reservation operation when the provided arguments are invalid. -/// -/// Message that describes how the arguments fail to meet expectations. -public record ReservationFailureOnInvalidArguments(string Message) : ReservationFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The arguments provided are invalid: {Message}"; -} +public record DisposedAllocationFailure() : Failure("The target allocation has been disposed."); /// /// Represents a failure in a memory reservation operation when no space is available within the allocated memory range /// to reserve the specified size. /// -public record ReservationFailureOnNoSpaceAvailable : ReservationFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => "No space is available within the allocated memory range to reserve the specified size."; -} \ No newline at end of file +public record InsufficientSpaceFailure() + : Failure("No space is available within the allocated memory range to reserve the specified size."); \ No newline at end of file diff --git a/src/MindControl/Results/Result.cs b/src/MindControl/Results/Result.cs index 4fb9309..ff01762 100644 --- a/src/MindControl/Results/Result.cs +++ b/src/MindControl/Results/Result.cs @@ -3,29 +3,24 @@ /// /// Represents the result of an operation that can either succeed or fail. /// -/// Type of the error that can be returned in case of failure. -public class Result +public class Result { - private readonly TError? _error; + private readonly Failure? _failure; /// Default string representing a success. - protected const string SuccessString = "Success"; - - /// Default string representing a failure. - protected const string FailureString = "An unspecified failure occurred"; + protected const string SuccessString = "The operation was successful."; /// - /// Gets a successful instance. + /// Gets a successful instance. /// - public static readonly Result Success = new(); + public static readonly Result Success = new(); /// - /// Gets the error that caused the operation to fail. Throws if the operation was successful. - /// Use this after checking to ensure the operation was not - /// successful. + /// Gets the failure from an unsuccessful result. Throws if the operation was successful. + /// Use this after checking to ensure the operation was not successful. /// - public TError Error => IsFailure ? _error! - : throw new InvalidOperationException("Cannot access the error of a successful result."); + public Failure Failure => IsFailure ? _failure! + : throw new InvalidOperationException("Cannot access the failure from a successful result."); /// /// Gets a boolean indicating if the operation was successful. @@ -38,24 +33,22 @@ public class Result public bool IsFailure => !IsSuccess; /// - /// Initializes a new successful instance. + /// Initializes a new successful instance. /// protected Result() { IsSuccess = true; } /// - /// Initializes a new failed instance. + /// Initializes a new failed instance. /// - /// Error that caused the operation to fail. - protected Result(TError error) + /// A description of the failure. + protected Result(Failure failure) { - _error = error; + _failure = failure; IsSuccess = false; } - /// - /// Throws a if the operation was not successful. - /// - public void ThrowOnError() + /// Throws a if the operation was not successful. + public void ThrowOnFailure() { if (IsFailure) throw ToException(); @@ -64,108 +57,108 @@ public void ThrowOnError() /// /// Converts the result to an exception if it represents a failure. /// - /// A new instance if the operation was a failure. + /// A new instance if the operation was a failure. /// Thrown if the operation was successful and thus cannot be /// converted to an exception. - public ResultFailureException ToException() => IsFailure ? new ResultFailureException(Error) + public ResultFailureException ToException() => IsFailure ? new ResultFailureException(Failure) : throw new InvalidOperationException("Cannot convert a successful result to an exception."); /// - /// Creates a new failed instance. + /// Creates a new failed instance. /// - /// Error that caused the operation to fail. - public static Result Failure(TError error) => new(error); + /// A description of the failure. + public static Result FailWith(Failure failure) => new(failure); /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => IsSuccess ? SuccessString : Error!.ToString() ?? FailureString; + => IsSuccess ? SuccessString : Failure.ToString(); /// - /// Implicitly converts a result value to a successful instance. + /// Implicitly converts a result value to a successful instance. /// /// Result value to convert. - public static implicit operator Result(TError result) => Failure(result); + public static implicit operator Result(Failure result) => FailWith(result); } /// /// Represents the result of an operation that can either succeed or fail, with a result value in case of success. /// /// Type of the result that can be returned in case of success. -/// Type of the error that can be returned in case of failure. -public class Result : Result +public class Result : Result { private readonly TResult? _value; /// /// Gets the resulting value of the operation. Throws if the operation was not successful. - /// Use this after checking to ensure the operation was successful. + /// Use this after checking to ensure the operation was successful. /// public TResult Value => IsSuccess ? _value! : throw new InvalidOperationException("Cannot access the value of an unsuccessful result.", - new ResultFailureException(Error)); + new ResultFailureException(Failure)); /// - /// Initializes a new successful instance. + /// Initializes a new successful instance. /// /// Result of the operation. protected Result(TResult value) { _value = value; } /// - /// Initializes a new failed instance. + /// Initializes a new failed instance. /// - /// Error that caused the operation to fail. - protected Result(TError error) : base(error) { } + /// A description of the failure. + protected Result(Failure failure) : base(failure) { } /// - /// Creates a new successful instance. + /// Creates a new successful instance. /// /// Result of the operation. - public static Result FromResult(TResult result) - => new(result); + public static Result FromResult(TResult result) => new(result); + + /// + /// Creates a new failed instance. + /// + /// A description of the failure. + public new static Result FailWith(Failure failure) => new(failure); /// - /// Creates a new failed instance. + /// Gets the resulting value of the operation, or the default value for the resulting type if the operation was not + /// successful. /// - /// Error that caused the operation to fail. - public new static Result Failure(TError error) => new(error); + public TResult? ValueOrDefault() => IsSuccess ? Value : default; /// - /// Gets the resulting value of the operation, or the default value if the operation was not successful. - /// You can optionally provide a specific default value to return if the operation was not successful. + /// Gets the resulting value of the operation, or the provided default value if the operation was not successful. /// - /// Default value to return if the operation was not successful. If not specified, - /// defaults to the default value for the result type. - public TResult? GetValueOrDefault(TResult? defaultValue = default) => IsSuccess ? Value : defaultValue; + /// Default value to return if the operation was not successful. + public TResult ValueOr(TResult defaultValue) => IsSuccess ? Value : defaultValue; /// - /// Builds a new instance from a successful result with a different type. + /// Builds a new instance from a successful result with a different type. /// /// Result to convert. /// Type of the value of the result to convert. - /// A new instance with the value of the input result. - public static Result CastValueFrom(Result result) + /// A new instance with the value of the input result. + public static Result CastValueFrom(Result result) where TOtherResult : TResult - => result.IsSuccess ? new Result(result.Value!) : result.Error; + => result.IsSuccess ? new Result(result.Value!) : result.Failure; /// - /// Implicitly converts a result value to a successful instance. + /// Implicitly converts a result value to a successful instance. /// /// Result value to convert. - public static implicit operator Result(TResult result) - => FromResult(result); + public static implicit operator Result(TResult result) => FromResult(result); /// - /// Implicitly converts an error to an unsuccessful instance. + /// Implicitly converts a failure to an unsuccessful instance. /// - /// Error to convert. - public static implicit operator Result(TError error) - => Failure(error); + /// Failure to convert. + public static implicit operator Result(Failure failure) => FailWith(failure); /// Returns a string that represents the current object. /// A string that represents the current object. public override string ToString() - => IsSuccess ? Value?.ToString() ?? SuccessString : Error?.ToString() ?? FailureString; + => IsSuccess ? Value?.ToString() ?? SuccessString : Failure.ToString(); } /// @@ -174,47 +167,43 @@ public override string ToString() /// too. /// /// Type of the result that can be returned in case of success. -/// Type of the error that can be returned in case of failure. -public class DisposableResult : Result, IDisposable where TResult : IDisposable +public class DisposableResult : Result, IDisposable where TResult : IDisposable { /// - /// Initializes a new successful instance. + /// Initializes a new successful instance. /// /// Result of the operation. protected DisposableResult(TResult value) : base(value) { } /// - /// Initializes a new failed instance. + /// Initializes a new failed instance. /// - /// Error that caused the operation to fail. - protected DisposableResult(TError error) : base(error) { } + /// A representation of the failure. + protected DisposableResult(Failure failure) : base(failure) { } /// - /// Implicitly converts a result value to a successful instance. + /// Implicitly converts a result value to a successful instance. /// /// Result value to convert. - public static implicit operator DisposableResult(TResult result) - => FromResult(result); + public static implicit operator DisposableResult(TResult result) => FromResult(result); /// - /// Implicitly converts an error to an unsuccessful instance. + /// Implicitly converts a failure to an unsuccessful instance. /// - /// Error to convert. - public static implicit operator DisposableResult(TError error) - => Failure(error); + /// Failure to convert. + public static implicit operator DisposableResult(Failure failure) => FailWith(failure); /// - /// Creates a new successful instance. + /// Creates a new successful instance. /// /// Result of the operation. - public new static DisposableResult FromResult(TResult result) - => new(result); + public new static DisposableResult FromResult(TResult result) => new(result); /// - /// Creates a new failed instance. + /// Creates a new failed instance. /// - /// Error that caused the operation to fail. - public new static DisposableResult Failure(TError error) => new(error); + /// A representation of the failure. + public new static DisposableResult FailWith(Failure failure) => new(failure); /// Disposes the result value if the operation was successful. public void Dispose() diff --git a/src/MindControl/Results/ResultFailureException.cs b/src/MindControl/Results/ResultFailureException.cs index b4523aa..0c77eed 100644 --- a/src/MindControl/Results/ResultFailureException.cs +++ b/src/MindControl/Results/ResultFailureException.cs @@ -1,32 +1,23 @@ namespace MindControl.Results; /// -/// Exception that may be thrown by failed . +/// Exception that may be thrown by failed . /// -public class ResultFailureException : Exception +public class ResultFailureException : Exception { - /// Default error message. - private const string DefaultMessage = "The operation failed."; - /// - /// Gets the error object describing the failure. + /// Gets a representation of the failure at the origin of the exception. /// - public T Error { get; } + public Failure Failure { get; } /// - /// Builds a with the given details. + /// Builds a with the given details. /// - /// Error object describing the failure. - public ResultFailureException(T error) : this(error, null) {} - - /// - /// Builds a with the given details. - /// - /// Error object describing the failure. + /// Representation of the failure at the origin of the exception. /// Exception that is the cause of this exception, if any. - public ResultFailureException(T error, Exception? innerException) - : base(error?.ToString() ?? DefaultMessage, innerException) + public ResultFailureException(Failure failure, Exception? innerException = null) + : base(failure.ToString(), innerException) { - Error = error; + Failure = failure; } } \ No newline at end of file diff --git a/src/MindControl/Results/StoreFailure.cs b/src/MindControl/Results/StoreFailure.cs deleted file mode 100644 index af10747..0000000 --- a/src/MindControl/Results/StoreFailure.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace MindControl.Results; - -/// Represents a failure in a memory store operation. -public abstract record StoreFailure; - -/// Represents a failure in a memory store operation when the target process is not attached. -public record StoreFailureOnDetachedProcess : StoreFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => Failure.DetachedErrorMessage; -} - -/// Represents a failure in a memory store operation when the provided arguments are invalid. -/// Message that describes how the arguments fail to meet expectations. -public record StoreFailureOnInvalidArguments(string Message) : StoreFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The arguments provided are invalid: {Message}"; -} - -/// Represents a failure in a memory store operation when the allocation operation failed. -/// The allocation failure that caused the store operation to fail. -public record StoreFailureOnAllocation(AllocationFailure Details) : StoreFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The allocation operation failed: {Details}"; -} - -/// Represents a failure in a memory store operation when the reservation operation failed. -/// The reservation failure that caused the store operation to fail. -public record StoreFailureOnReservation(ReservationFailure Details) : StoreFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The reservation operation failed: {Details}"; -} - -/// Represents a failure in a memory store operation when the write operation failed. -/// The write failure that caused the store operation to fail. -public record StoreFailureOnWrite(WriteFailure Details) : StoreFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The write operation failed: {Details}"; -} \ No newline at end of file diff --git a/src/MindControl/Results/StringReadFailure.cs b/src/MindControl/Results/StringReadFailure.cs index c410837..705a341 100644 --- a/src/MindControl/Results/StringReadFailure.cs +++ b/src/MindControl/Results/StringReadFailure.cs @@ -1,93 +1,25 @@ namespace MindControl.Results; -/// Represents a failure in a string read operation. -public abstract record StringReadFailure; - -/// Represents a failure in a string read operation when the target process is not attached. -public record StringReadFailureOnDetachedProcess : StringReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => Failure.DetachedErrorMessage; -} - -/// Represents a failure in a string read operation when the pointer path evaluation failed. -/// Details about the path evaluation failure. -public record StringReadFailureOnPointerPathEvaluation(PathEvaluationFailure Details) : StringReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"Failed to evaluate the specified pointer path: {Details}"; -} - -/// Represents a failure in a string read operation when resolving the address in the target process. -/// Details about the address resolution failure. -/// Type of the underlying failure. -public record StringReadFailureOnAddressResolution(T Details) : StringReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"Failed to resolve the address: {Details}"; -} - /// Represents a failure in a string read operation when the settings provided are invalid. -public record StringReadFailureOnInvalidSettings : StringReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => "The provided string settings are invalid. They must specify either a length prefix or a null terminator."; -} - -/// -/// Represents a failure in a string read operation when the target process is 32-bit, but the target memory address is -/// not within the 32-bit address space. -/// -/// Address that caused the failure. -public record StringReadFailureOnIncompatibleBitness(UIntPtr Address) : StringReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The target address {Address} is not within the 32-bit address space."; -} - -/// Represents a failure in a string read operation when the target pointer is a zero pointer. -public record StringReadFailureOnZeroPointer : StringReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => "The target address is a zero pointer."; -} - -/// Represents a failure in a string read operation when the pointer read operation failed. -/// Details about the pointer read failure. -public record StringReadFailureOnPointerReadFailure(ReadFailure Details) : StringReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The pointer read operation failed: {Details}"; -} - -/// Represents a failure in a string read operation when a read operation on the string bytes failed. -/// Address where the string read operation failed. -/// Details about the read failure. -public record StringReadFailureOnStringBytesReadFailure(UIntPtr Address, ReadFailure Details) : StringReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The string read operation failed at address {Address}: {Details}"; -} +public record InvalidStringSettingsFailure() + : Failure("The provided string settings are invalid. They must specify either a length prefix or a null terminator."); /// /// Represents a failure in a string read operation when the string length prefix was evaluated to a value exceeding the /// configured max length, or a null terminator was not found within the configured max length. /// /// Length read from the length prefix bytes, in case a length prefix was set. -public record StringReadFailureOnStringTooLong(ulong? LengthPrefixValue) : StringReadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => LengthPrefixValue != null +public record StringTooLongFailure(ulong? LengthPrefixValue) : Failure(LengthPrefixValue != null ? $"The string was found with a length prefix of {LengthPrefixValue}, which exceeds the configured max length." - : "String reading was aborted because no null terminator was found within the configured max length."; + : "String reading was aborted because no null terminator was found within the configured max length.") +{ + /// Length read from the length prefix bytes, in case a length prefix was set. + public ulong? LengthPrefixValue { get; init; } = LengthPrefixValue; } + +/// +/// Represents a failure in a string settings search operation when no adequate settings were found to read the given +/// string from the specified pointer. +/// +public record UndeterminedStringSettingsFailure() + : Failure("No adequate settings were found to read the given string from the specified pointer."); diff --git a/src/MindControl/Results/SystemFailure.cs b/src/MindControl/Results/SystemFailure.cs index 9bcd55d..299ed15 100644 --- a/src/MindControl/Results/SystemFailure.cs +++ b/src/MindControl/Results/SystemFailure.cs @@ -1,26 +1,27 @@ namespace MindControl.Results; -/// Represents a failure in an operating system operation. -public abstract record SystemFailure; - -/// Represents a failure in an operating system operation when the provided arguments are invalid. -/// Name of the argument that caused the failure. -/// Message that describes how the argument fails to meet expectations. -public record SystemFailureOnInvalidArgument(string ArgumentName, string Message) : SystemFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"The value provided for \"{ArgumentName}\" is invalid: {Message}"; -} - /// Represents a failure in a system API call. +/// Name of the system API function that failed. +/// Friendly name of the operation that was attempted. As multiple system API calls +/// may be made to perform a single manipulation, this name is used to identify the broader operation that failed. +/// /// Numeric code that identifies the error. Typically provided by the operating system. -/// Message that describes the error. Typically provided by the operating system. -public record OperatingSystemCallFailure(int ErrorCode, string ErrorMessage) : SystemFailure +/// Message that describes the error, provided by the operating system. +public record OperatingSystemCallFailure(string SystemApiName, string TopLevelOperationName, int ErrorCode, + string SystemMessage) + : Failure($"A system API call to {SystemApiName} as part of a {TopLevelOperationName} operation failed with error code {ErrorCode}: {SystemMessage}") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"A system API call failed with error code {ErrorCode}: {ErrorMessage}"; + /// Name of the system API function that failed. + public string SystemApiName { get; init; } = SystemApiName; + + /// Friendly name of the operation that was attempted. As multiple system API calls + /// may be made to perform a single manipulation, this name is used to identify the broader operation that failed. + /// + public string TopLevelOperationName { get; init; } = TopLevelOperationName; + + /// Numeric code that identifies the error. Typically provided by the operating system. + public int ErrorCode { get; init; } = ErrorCode; + + /// Message that describes the error, provided by the operating system. + public string SystemMessage { get; init; } = SystemMessage; } \ No newline at end of file diff --git a/src/MindControl/Results/ThreadFailure.cs b/src/MindControl/Results/ThreadFailure.cs index d9ddf67..b5d59a2 100644 --- a/src/MindControl/Results/ThreadFailure.cs +++ b/src/MindControl/Results/ThreadFailure.cs @@ -1,79 +1,16 @@ namespace MindControl.Results; -/// Represents a failure in a thread operation. -public record ThreadFailure; - -/// Represents a failure in a thread operation when the target process is not attached. -public record ThreadFailureOnDetachedProcess : ThreadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => Failure.DetachedErrorMessage; -} - -/// Represents a failure in a thread operation when the arguments provided are invalid. -/// Message that describes how the arguments fail to meet expectations. -public record ThreadFailureOnInvalidArguments(string Message) : ThreadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The arguments provided are invalid: {Message}"; -} - /// Represents a failure in a thread operation when the thread handle has already been disposed. -public record ThreadFailureOnDisposedInstance : ThreadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => "The thread handle has already been disposed."; -} - -/// Represents a failure in a thread operation when evaluating the pointer path to the target address. -/// -/// Details about the failure. -public record ThreadFailureOnPointerPathEvaluation(PathEvaluationFailure Details) : ThreadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"Failure when evaluating the pointer path to the target address: {Details}"; -} +public record DisposedThreadFailure() : Failure("The thread handle has already been disposed."); /// Represents a failure in a thread operation when the target function cannot be found. /// Message including details about the failure. -public record ThreadFailureOnFunctionNotFound(string Message) : ThreadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"Could not find the target function. {Message}"; -} +public record FunctionNotFoundFailure(string Message) : Failure($"Could not find the target function. {Message}"); /// /// Represents a failure in a thread operation when the thread did not finish execution within the specified timeout. /// -public record ThreadFailureOnWaitTimeout : ThreadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => "The thread did not finish execution within the specified timeout."; -} +public record ThreadWaitTimeoutFailure() : Failure("The thread did not terminate within the specified time frame."); /// Represents a failure in a thread operation when a waiting operation was abandoned. -public record ThreadFailureOnWaitAbandoned : ThreadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => "The waiting operation was abandoned."; -} - -/// -/// Represents a failure in a thread operation when invoking a system API function. -/// -/// Message that details what operation failed. -/// Details about the failure. -public record ThreadFailureOnSystemFailure(string Message, SystemFailure Details) : ThreadFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"A system API function call failed: {Message} / {Details}"; -} \ No newline at end of file +public record ThreadWaitAbandonedFailure() : Failure("The waiting operation was abandoned."); \ No newline at end of file diff --git a/src/MindControl/Results/WriteFailure.cs b/src/MindControl/Results/WriteFailure.cs index dcd0d54..9a0b250 100644 --- a/src/MindControl/Results/WriteFailure.cs +++ b/src/MindControl/Results/WriteFailure.cs @@ -1,57 +1,13 @@ namespace MindControl.Results; -/// Represents a failure in a memory write operation. -public abstract record WriteFailure; - -/// Represents a failure in a memory write operation when the target process is not attached. -public record WriteFailureOnDetachedProcess : WriteFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => Failure.DetachedErrorMessage; -} - -/// -/// Represents a failure in a memory write operation when evaluating the pointer path to the target memory. -/// -/// Details about the failure. -public record WriteFailureOnPointerPathEvaluation(PathEvaluationFailure Details) : WriteFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"Failed to evaluate the specified pointer path: {Details}"; -} - -/// -/// Represents a failure in a memory write operation when resolving the address in the target process. -/// -/// Details about the failure. -/// Type of the underlying failure. -public record WriteFailureOnAddressResolution(T Details) : WriteFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"Failed to resolve the address: {Details}"; -} - -/// Represents a failure in a memory write operation when the arguments provided are invalid. -/// Message that describes how the arguments fail to meet expectations. -public record WriteFailureOnInvalidArguments(string Message) : WriteFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The arguments provided are invalid: {Message}"; -} - /// /// Represents a failure in a memory write operation when the value to write cannot be converted to an array of bytes. /// /// Type that caused the failure. -public record WriteFailureOnUnsupportedType(Type Type) : WriteFailure +public record UnsupportedTypeWriteFailure(Type Type) : Failure($"The type {Type} is not supported for writing.") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"The type {Type} is not supported for writing."; + /// Type that caused the failure. + public Type Type { get; init; } = Type; } /// @@ -59,20 +15,10 @@ public record WriteFailureOnUnsupportedType(Type Type) : WriteFailure /// is not within the 32-bit address space. /// /// Address that caused the failure. -public record WriteFailureOnIncompatibleBitness(UIntPtr Address) : WriteFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"The pointer to write, {Address}, is too large for a 32-bit process. If you want to write an 8-byte value and not a memory address, use a ulong instead."; -} - -/// Represents a failure in a memory write operation when the address to write is a zero pointer. -public record WriteFailureOnZeroPointer : WriteFailure +public record IncompatibleBitnessWriteFailure(UIntPtr Address) : Failure($"The pointer to write, {Address}, is too large for a 32-bit process. If you want to write an 8-byte value and not a memory address, use a ulong instead.") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => "The address to write is a zero pointer."; + /// Address that caused the failure. + public UIntPtr Address { get; init; } = Address; } /// @@ -80,13 +26,15 @@ public record WriteFailureOnZeroPointer : WriteFailure /// the target memory space fails. /// /// Address where the operation failed. -/// Details about the failure. -public record WriteFailureOnSystemProtectionRemoval(UIntPtr Address, SystemFailure Details) : WriteFailure +/// A description of the inner failure. +public record MemoryProtectionRemovalFailure(UIntPtr Address, Failure Details) + : Failure($"Failed to remove the memory protection at address {Address:X}: \"{Details}\".{Environment.NewLine}Change the memory protection strategy to {nameof(MemoryProtectionStrategy)}.{nameof(MemoryProtectionStrategy.Ignore)} to prevent memory protection removal. Because this is the first step when writing a value, this failure may also indicate that the target address is not within a valid memory range.") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"Failed to remove the protection of the memory at address {Address:X}: \"{Details}\".{Environment.NewLine}Change the memory protection strategy to {nameof(MemoryProtectionStrategy)}.{nameof(MemoryProtectionStrategy.Ignore)} to prevent memory protection removal. As protection removal is the first step when writing a value, it may simply be that the provided target address does not point to valid memory."; + /// Address where the operation failed. + public UIntPtr Address { get; init; } = Address; + + /// A description of the inner failure. + public Failure Details { get; init; } = Details; } /// @@ -95,24 +43,14 @@ public override string ToString() /// /// Address where the operation failed. /// Details about the failure. -public record WriteFailureOnSystemProtectionRestoration(UIntPtr Address, SystemFailure Details) : WriteFailure -{ - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"The value was written successfully, but the protection of the memory at address {Address} could not be restored to its original value: {Details}.{Environment.NewLine}Change the memory protection strategy to {nameof(MemoryProtectionStrategy)}.{nameof(MemoryProtectionStrategy.Remove)} to prevent memory protection restoration."; -} - -/// -/// Represents a failure in a memory write operation when the system API call to write bytes in memory fails. -/// -/// Address where the write operation failed. -/// Details about the failure. -public record WriteFailureOnSystemWrite(UIntPtr Address, SystemFailure Details) : WriteFailure +public record MemoryProtectionRestorationFailure(UIntPtr Address, Failure Details) + : Failure($"The value was written successfully, but the memory protection at address {Address} could not be restored to its original value: {Details}.{Environment.NewLine}Change the memory protection strategy to {nameof(MemoryProtectionStrategy)}.{nameof(MemoryProtectionStrategy.Remove)} to skip memory protection restoration if you don't need it.") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() => $"Failed to write at the address {Address}: {Details}"; + /// Address where the operation failed. + public UIntPtr Address { get; init; } = Address; + + /// Details about the failure. + public Failure Details { get; init; } = Details; } /// @@ -121,10 +59,9 @@ public record WriteFailureOnSystemWrite(UIntPtr Address, SystemFailure Details) /// /// Type that caused the failure. /// Exception that occurred during the conversion. -public record WriteFailureOnConversion(Type Type, Exception ConversionException) : WriteFailure +public record ConversionWriteFailure(Type Type, Exception ConversionException) + : Failure($"Failed to convert the value of type {Type} to an array of bytes. Make sure the type has a fixed length. See the ConversionException property for more details.") { - /// Returns a string that represents the current object. - /// A string that represents the current object. - public override string ToString() - => $"Failed to convert the value of type {Type} to an array of bytes. Make sure the type has a fixed length. See the ConversionException property for more details."; + /// Type that caused the failure. + public Type Type { get; init; } = Type; } diff --git a/src/MindControl/Search/ByteSearchPattern.cs b/src/MindControl/Search/ByteSearchPattern.cs index 5e59b52..1bd0431 100644 --- a/src/MindControl/Search/ByteSearchPattern.cs +++ b/src/MindControl/Search/ByteSearchPattern.cs @@ -38,7 +38,7 @@ public ByteSearchPattern(string patternString) { var parseResult = ParsePatternString(patternString); if (parseResult.IsFailure) - throw new ArgumentException(parseResult.Error.ToString(), nameof(patternString)); + throw new ArgumentException(parseResult.Failure.ToString(), nameof(patternString)); _originalPatternString = patternString; (byte[] byteArray, byte[] mask) = parseResult.Value; @@ -66,13 +66,12 @@ protected ByteSearchPattern(string originalPatternString, byte[] byteArray, byte /// of hexadecimal bytes, optionally separated by spaces. Each character, excluding spaces, can be a specific value /// (0-F) or a wildcard "?" character, indicating that the value to look for at this position could be any value. /// An example would be "1F ?? 4B 00 ?6". Read the documentation for more information. - /// A result holding either the parsed , or an instance of - /// detailing the reason for the failure. - public static Result TryParse(string patternString) + /// A result holding either the parsed , or a failure. + public static Result TryParse(string patternString) { var parseResult = ParsePatternString(patternString); if (parseResult.IsFailure) - return parseResult.Error; + return parseResult.Failure; (byte[] byteArray, byte[] mask) = parseResult.Value; return new ByteSearchPattern(patternString, byteArray, mask); @@ -82,9 +81,9 @@ public static Result TryParse(stri /// Parses a pattern string into a byte array and a mask array. /// /// Pattern string to parse. - /// A tuple containing the byte array and the mask array parsed from the pattern string, or an instance of - /// detailing the reason for the failure. - private static Result, InvalidBytePatternFailure> ParsePatternString( + /// A tuple containing the byte array and the mask array parsed from the pattern string, or a failure. + /// + private static Result> ParsePatternString( string patternString) { if (string.IsNullOrWhiteSpace(patternString)) diff --git a/src/MindControl/Threading/RemoteThread.cs b/src/MindControl/Threading/RemoteThread.cs index 609db28..fd6b22c 100644 --- a/src/MindControl/Threading/RemoteThread.cs +++ b/src/MindControl/Threading/RemoteThread.cs @@ -25,16 +25,16 @@ internal RemoteThread(IOperatingSystemService osService, IntPtr threadHandle) /// Synchronously waits for the thread to finish execution and returns its exit code. /// /// Maximum time to wait for the thread to finish execution. If the duration is exceeded, the - /// result will contain a error. - /// A result holding either the exit code of the thread, or a error. - public Result WaitForCompletion(TimeSpan timeout) + /// result will hold a instance. + /// A result holding either the exit code of the thread, or a failure. + public Result WaitForCompletion(TimeSpan timeout) { if (IsDisposed) - return new ThreadFailureOnDisposedInstance(); + return new DisposedThreadFailure(); var waitResult = _osService.WaitThread(_threadHandle, timeout); if (waitResult.IsFailure) - return waitResult.Error; + return waitResult.Failure; Dispose(); return waitResult.Value; } @@ -44,10 +44,9 @@ public Result WaitForCompletion(TimeSpan timeout) /// asynchronous wrapper around . /// /// Maximum time to wait for the thread to finish execution. If the duration is exceeded, the - /// result will contain a error. - /// A result holding either the exit code of the thread, or a error. - public Task> WaitForCompletionAsync(TimeSpan timeout) - => Task.Run(() => WaitForCompletion(timeout)); + /// result will contain a error. + /// A result holding either the exit code of the thread, or a failure. + public Task> WaitForCompletionAsync(TimeSpan timeout) => Task.Run(() => WaitForCompletion(timeout)); /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged /// resources. diff --git a/test/MindControl.Benchmark/BenchmarkMemorySetup.cs b/test/MindControl.Benchmark/BenchmarkMemorySetup.cs index b95ce9c..677503d 100644 --- a/test/MindControl.Benchmark/BenchmarkMemorySetup.cs +++ b/test/MindControl.Benchmark/BenchmarkMemorySetup.cs @@ -58,7 +58,7 @@ public static BenchmarkMemorySetup Setup() if (!UIntPtr.TryParse(line, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var outerClassPointer)) throw new Exception($"Could not read the outer class pointer output by the app: \"{line}\"."); - var mindControlProcessMemory = ProcessMemory.OpenProcessById(targetProcess.Id); + var mindControlProcessMemory = ProcessMemory.OpenProcessById(targetProcess.Id).Value; var memoryDllMem = new Mem(); memoryDllMem.OpenProcess(targetProcess.Id); diff --git a/test/MindControl.Benchmark/Benchmarks/ReadIntByAddressBenchmark.cs b/test/MindControl.Benchmark/Benchmarks/ReadIntByAddressBenchmark.cs index 97ab48a..871bb11 100644 --- a/test/MindControl.Benchmark/Benchmarks/ReadIntByAddressBenchmark.cs +++ b/test/MindControl.Benchmark/Benchmarks/ReadIntByAddressBenchmark.cs @@ -24,6 +24,17 @@ public int MindControlReadGenericType() return result; } + [Benchmark(Description = "MindControl 100xRead")] + public void MindControlMassReadGenericType() + { + for (int i = 0; i < 100; i++) + { + var result = _setup.MindControlProcessMemory.Read(_setup.OuterClassPointer + 0x38).Value; + if (result != -7651) + throw new Exception("Unexpected result"); + } + } + [Benchmark(Description = "MindControl Read(Type)")] public int MindControlReadObject() { @@ -52,4 +63,16 @@ public int MemoryReadGeneric() throw new Exception("Unexpected result"); return result; } + + [Benchmark(Description = "Memory.dll 100xReadMemory")] + public void MemoryMassReadGeneric() + { + for (int i = 0; i < 100; i++) + { + UIntPtr address = _setup.OuterClassPointer + 0x38; + var result = _setup.MemoryDllMem.ReadMemory(address.ToString("X")); + if (result != -7651) + throw new Exception("Unexpected result"); + } + } } \ No newline at end of file diff --git a/test/MindControl.Benchmark/Benchmarks/ReadLongByPointerPathBenchmark.cs b/test/MindControl.Benchmark/Benchmarks/ReadLongByPointerPathBenchmark.cs index ffa165c..dde4cee 100644 --- a/test/MindControl.Benchmark/Benchmarks/ReadLongByPointerPathBenchmark.cs +++ b/test/MindControl.Benchmark/Benchmarks/ReadLongByPointerPathBenchmark.cs @@ -58,6 +58,29 @@ public long MindControlReadObjectDynamic() return result; } + [Benchmark(Description = "MindControl 100xRead")] + public void MindControl100xReadGenericTypeReuse() + { + for (int i = 0; i < 100; i++) + { + long result = _setup.MindControlProcessMemory.Read(_pointerPath).Value; + if (result != -1) + throw new Exception("Unexpected result"); + } + } + + [Benchmark(Description = "MindControl 100xRead (module path)")] + public void MindControl100xReadGenericModulePath() + { + var modulePath = new PointerPath("MindControl.Test.TargetApp.dll+8"); + for (int i = 0; i < 100; i++) + { + long result = _setup.MindControlProcessMemory.Read(modulePath).Value; + if (result == 0) + throw new Exception("Unexpected result"); + } + } + [Benchmark(Description = "Memory.dll ReadLong", Baseline = true)] public long MemoryReadLong() { @@ -75,4 +98,26 @@ public long MemoryReadGeneric() throw new Exception("Unexpected result"); return result; } + + [Benchmark(Description = "Memory.dll 100xReadMemory")] + public void Memory100xReadGeneric() + { + for (int i = 0; i < 100; i++) + { + var result = _setup.MemoryDllMem.ReadMemory(_pathString); + if (result != -1) + throw new Exception("Unexpected result"); + } + } + + [Benchmark(Description = "Memory.dll 100xReadMemory (module path)")] + public void Memory100xReadGenericWithModule() + { + for (int i = 0; i < 100; i++) + { + var result = _setup.MemoryDllMem.ReadMemory("MindControl.Test.TargetApp.dll+8"); + if (result != -1) + throw new Exception("Unexpected result"); + } + } } \ No newline at end of file diff --git a/test/MindControl.Benchmark/MindControl.Benchmark.csproj b/test/MindControl.Benchmark/MindControl.Benchmark.csproj index ce784a0..8f4a88d 100644 --- a/test/MindControl.Benchmark/MindControl.Benchmark.csproj +++ b/test/MindControl.Benchmark/MindControl.Benchmark.csproj @@ -15,10 +15,15 @@ - + + + + bin\Debug\net8.0\Memory.dll + + \ No newline at end of file diff --git a/test/MindControl.Test/AddressingTests/PointerPathTest.cs b/test/MindControl.Test/AddressingTests/PointerPathTest.cs index e4b7786..e13e618 100644 --- a/test/MindControl.Test/AddressingTests/PointerPathTest.cs +++ b/test/MindControl.Test/AddressingTests/PointerPathTest.cs @@ -420,4 +420,27 @@ private void AssertResultingAddress(PointerPath? result, ExpressionTestCase test Assert.That(result.IsStrictly64Bit, Is.EqualTo(testCase.Expect64BitOnly), testCase.Explanation); }); } + + /// + /// Builds a pointer path from direct pointers (as opposed to an expression), and gets the expression from it. + /// Checks that the expression matches expectations. + /// + [Test] + public void GetExpressionFromDirectPointerPathTest() + { + var pointerPath = new PointerPath(new UIntPtr(0x1F016644), 0x4, -0x1C); + Assert.That(pointerPath.Expression, Is.EqualTo("1F016644,4,-1C")); + } + + /// + /// Builds a pointer path from a module name, a module offset, and a sequence of pointer offsets, and gets the + /// expression from it. + /// Checks that the expression matches expectations. + /// + [Test] + public void GetExpressionFromDirectPointerPathWithModuleNameTest() + { + var pointerPath = new PointerPath("mygame.exe", 0xC16, 0x4, -0x1C); + Assert.That(pointerPath.Expression, Is.EqualTo("\"mygame.exe\"+C16,4,-1C")); + } } \ No newline at end of file diff --git a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs index c763062..386eb3a 100644 --- a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs +++ b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs @@ -39,13 +39,13 @@ public void CanReadTest() /// /// Tests the getter. - /// Must always return false. + /// Must always return true. /// [Test] public void CanSeekTest() { using var stream = TestProcessMemory!.GetMemoryStream(OuterClassPointer); - Assert.That(stream.CanSeek, Is.False); + Assert.That(stream.CanSeek, Is.True); } /// diff --git a/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs b/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs index 846efa3..19b1b1c 100644 --- a/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs +++ b/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs @@ -39,7 +39,7 @@ public void DisposeTest() Assert.That(_allocation.IsDisposed, Is.True); var reserveAttemptResult = _allocation.ReserveRange(0x10); Assert.That(reserveAttemptResult.IsSuccess, Is.False); - Assert.That(reserveAttemptResult.Error, Is.TypeOf()); + Assert.That(reserveAttemptResult.Failure, Is.TypeOf()); // Check that the allocation has been removed from the list Assert.That(TestProcessMemory!.Allocations, Is.Empty); @@ -83,14 +83,14 @@ public void ReserveRangeWithFullRangeTest() /// /// Tests the method. - /// Attempt to reserve 0 bytes. This should return a result. + /// Attempt to reserve 0 bytes. This should return a result. /// [Test] public void ReserveRangeWithZeroSizeTest() { var reservationResult = _allocation.ReserveRange(0); Assert.That(reservationResult.IsSuccess, Is.False); - Assert.That(reservationResult.Error, Is.TypeOf()); + Assert.That(reservationResult.Failure, Is.TypeOf()); } /// @@ -102,7 +102,7 @@ public void ReserveRangeWithRangeLargerThanAllocationTest() { var result = _allocation.ReserveRange(0x1001); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); + Assert.That(result.Failure, Is.TypeOf()); } /// @@ -116,7 +116,7 @@ public void ReserveRangeWithMultipleReservationsTooLargeToFitTest() _allocation.ReserveRange(0x100); var result = _allocation.ReserveRange(0x1000); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); + Assert.That(result.Failure, Is.TypeOf()); } /// diff --git a/test/MindControl.Test/AnchorTests/ValueWatcherTest.cs b/test/MindControl.Test/AnchorTests/ValueWatcherTest.cs index e7bc46e..a621e9f 100644 --- a/test/MindControl.Test/AnchorTests/ValueWatcherTest.cs +++ b/test/MindControl.Test/AnchorTests/ValueWatcherTest.cs @@ -1,27 +1,26 @@ using MindControl.Anchors; -using MindControl.Results; using MindControl.Test.ProcessMemoryTests; using NUnit.Framework; namespace MindControl.Test.AnchorTests; -/// Tests . +/// Tests . [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] public class ValueWatcherTest : BaseProcessMemoryTest { /// Builds and returns an anchor instance that can be used to build a value watcher. - private ValueAnchor GetAnchorOnOutputInt() + private ValueAnchor GetAnchorOnOutputInt() => TestProcessMemory!.GetAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); /// Builds and returns a value watcher instance that can be used to test the class. - private ValueWatcher GetWatcherOnOutputInt(TimeSpan? refreshInterval = null) + private ValueWatcher GetWatcherOnOutputInt(TimeSpan? refreshInterval = null) => GetAnchorOnOutputInt().Watch(refreshInterval ?? DefaultRefreshInterval); /// Default interval for refreshing the value in the watcher. private static readonly TimeSpan DefaultRefreshInterval = TimeSpan.FromSeconds(5); /// - /// Tests the constructor. + /// Tests the constructor. /// The constructor should set the watcher instance to an initial state by reading the value without raising events. /// [Test] @@ -43,7 +42,7 @@ public void ReadableInitialStateTest() } /// - /// Tests the constructor. + /// Tests the constructor. /// The watcher is set to an initial state where the value is unreadable. /// [Test] @@ -64,7 +63,7 @@ public void UnreadableInitialStateTest() } /// - /// Tests the event. + /// Tests the event. /// Build a value watcher, let the process change the value, force an update, and check if the event is raised. /// [Test] @@ -94,7 +93,7 @@ public void ValueChangedTest() } /// - /// Tests the event. + /// Tests the event. /// Build a value watcher with a pointer path, change a pointer in the path so that the address no longer resolves, /// and check that the event gets raised after a state update. /// @@ -102,9 +101,9 @@ public void ValueChangedTest() public void ValueLostTest() { using var reservation = TestProcessMemory!.Reserve(16, false).Value; - TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnError(); + TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnFailure(); int targetValue = 46; - TestProcessMemory.Write(reservation.Address + 8, targetValue).ThrowOnError(); + TestProcessMemory.Write(reservation.Address + 8, targetValue).ThrowOnFailure(); var pointerPath = new PointerPath($"{reservation.Address:X},0"); var watcher = TestProcessMemory.GetAnchor(pointerPath).Watch(DefaultRefreshInterval); @@ -123,7 +122,7 @@ public void ValueLostTest() Assert.That(watcher.IsValueReadable, Is.True); Assert.That(watcher.LastKnownValue, Is.EqualTo(targetValue)); - TestProcessMemory.Write(reservation.Address, 0).ThrowOnError(); // Sabotage the pointer path + TestProcessMemory.Write(reservation.Address, 0).ThrowOnFailure(); // Sabotage the pointer path watcher.UpdateState(); watcher.UpdateState(); // Update once more to make sure events are not raised multiple times @@ -135,7 +134,7 @@ public void ValueLostTest() } /// - /// Tests the event. + /// Tests the event. /// Build a value watcher with a pointer path, change a pointer in the path so that the address no longer resolves, /// update the state so that the value gets lost, and then repair the pointer path and update the state again. /// The tested event should be raised exactly once. @@ -144,9 +143,9 @@ public void ValueLostTest() public void ValueReacquiredTest() { using var reservation = TestProcessMemory!.Reserve(16, false).Value; - TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnError(); + TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnFailure(); int targetValue = 46; - TestProcessMemory.Write(reservation.Address + 8, targetValue).ThrowOnError(); + TestProcessMemory.Write(reservation.Address + 8, targetValue).ThrowOnFailure(); var pointerPath = new PointerPath($"{reservation.Address:X},0"); var watcher = TestProcessMemory.GetAnchor(pointerPath).Watch(DefaultRefreshInterval); @@ -164,10 +163,10 @@ public void ValueReacquiredTest() Assert.That(watcher.IsValueReadable, Is.True); Assert.That(watcher.LastKnownValue, Is.EqualTo(targetValue)); - TestProcessMemory.Write(reservation.Address, 0).ThrowOnError(); // Sabotage the pointer path + TestProcessMemory.Write(reservation.Address, 0).ThrowOnFailure(); // Sabotage the pointer path watcher.UpdateState(); // This should raise a ValueLost event - TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnError(); // Repair the pointer path + TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnFailure(); // Repair the pointer path watcher.UpdateState(); // This should raise a ValueReacquired event watcher.UpdateState(); // Update once more to make sure events are not raised multiple times watcher.Dispose(); @@ -178,8 +177,8 @@ public void ValueReacquiredTest() } /// - /// Tests the and - /// events. + /// Tests the and + /// events. /// Same setup as , except we make the pointer path resolve to a different value. /// We expect the ValueReacquired event to be raised first, and then the ValueChanged event. /// @@ -187,9 +186,9 @@ public void ValueReacquiredTest() public void ValueReacquiredAndChangedTest() { using var reservation = TestProcessMemory!.Reserve(16, false).Value; - TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnError(); + TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnFailure(); int targetValue = 46; - TestProcessMemory.Write(reservation.Address + 8, targetValue).ThrowOnError(); + TestProcessMemory.Write(reservation.Address + 8, targetValue).ThrowOnFailure(); var pointerPath = new PointerPath($"{reservation.Address:X},0"); var watcher = TestProcessMemory.GetAnchor(pointerPath).Watch(DefaultRefreshInterval); @@ -218,11 +217,11 @@ public void ValueReacquiredAndChangedTest() Assert.That(watcher.IsValueReadable, Is.True); Assert.That(watcher.LastKnownValue, Is.EqualTo(targetValue)); - TestProcessMemory.Write(reservation.Address, 0).ThrowOnError(); // Sabotage the pointer path + TestProcessMemory.Write(reservation.Address, 0).ThrowOnFailure(); // Sabotage the pointer path watcher.UpdateState(); // This should raise a ValueLost event // Make the pointer path resolve to an area with a 0 value - TestProcessMemory.Write(reservation.Address, reservation.Address + 12).ThrowOnError(); + TestProcessMemory.Write(reservation.Address, reservation.Address + 12).ThrowOnFailure(); watcher.UpdateState(); // This should raise a ValueReacquired event and then a ValueChanged event watcher.Dispose(); diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs index 5fb6797..2a9ac58 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs @@ -92,46 +92,44 @@ public void DisableCodeAtRevertTest() /// /// Tests the method. /// The method is called with a zero pointer. - /// Expects a . + /// Expects a . /// [Test] public void DisableCodeAtWithZeroAddressTest() { var result = TestProcessMemory!.DisableCodeAt(UIntPtr.Zero); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); + Assert.That(result.Failure, Is.TypeOf()); } /// /// Tests the method. /// The method is called with an instruction count of zero. - /// Expects a . + /// Expects a . /// [Test] public void DisableCodeAtWithInvalidInstructionCountTest() { var result = TestProcessMemory!.DisableCodeAt(FindMovIntAddress(), 0); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); + Assert.That(result.Failure, Is.TypeOf()); } /// /// Tests the method. - /// The method is called with a pointer path that does not point to a valid address. - /// Expects a . + /// The method is called with a pointer path that does not point to a valid address. Expects a failure. /// [Test] public void DisableCodeAtWithBadPointerPathTest() { var result = TestProcessMemory!.DisableCodeAt("bad pointer path"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); } /// /// Tests the method. /// The method is called on a freshly allocated memory address that holds only FF bytes instead of valid code. - /// Expects a . + /// Expects a . /// [Test] public void DisableCodeAtWithBadInstructionsTest() @@ -140,12 +138,12 @@ public void DisableCodeAtWithBadInstructionsTest() TestProcessMemory.WriteBytes(address, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }); // Write invalid code var result = TestProcessMemory.DisableCodeAt(address); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); + Assert.That(result.Failure, Is.TypeOf()); } /// /// Tests the method with a - /// detached process. Expects a . + /// detached process. Expects a . /// [Test] public void DisableCodeWithDetachedProcessTest() @@ -153,12 +151,12 @@ public void DisableCodeWithDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory!.DisableCodeAt(FindMovIntAddress()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); + Assert.That(result.Failure, Is.TypeOf()); } /// /// Tests the method with a - /// detached process. Expects a . + /// detached process. Expects a . /// [Test] public void DisableCodeWithPointerPathWithDetachedProcessTest() @@ -166,7 +164,7 @@ public void DisableCodeWithPointerPathWithDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory!.DisableCodeAt(FindMovIntAddress().ToString("X")); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); + Assert.That(result.Failure, Is.TypeOf()); } } @@ -181,7 +179,7 @@ public class ProcessMemoryCodeExtensionsTestX86 : ProcessMemoryCodeExtensionsTes /// /// Tests with an x64 address on - /// an x86 process. Expects a . + /// an x86 process. Expects a . /// [Test] public void DisableCodeAtX64AddressOnX86ProcessTest() @@ -189,6 +187,6 @@ public void DisableCodeAtX64AddressOnX86ProcessTest() var address = (ulong)uint.MaxValue + 1; var result = TestProcessMemory!.DisableCodeAt(new UIntPtr(address)); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); + Assert.That(result.Failure, Is.TypeOf()); } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs index c378602..fb6055f 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs @@ -146,7 +146,7 @@ public void HookAndInsertMovWithRegisterIsolationWithAssemblerTest() /// /// Tests the method. /// The method is called with a zero pointer address. - /// Expects a result. + /// Expects a result. /// [Test] public void HookWithZeroAddressTest() @@ -154,13 +154,12 @@ public void HookWithZeroAddressTest() var hookResult = TestProcessMemory!.Hook(UIntPtr.Zero, new byte[5], new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsFailure, Is.True); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests the method. - /// The method is called with a pointer path that does not evaluate to a valid address. - /// Expects a result. + /// The method is called with a pointer path that does not evaluate to a valid address. Expects a failure. /// [Test] public void HookWithBadPathWithByteArrayTest() @@ -168,15 +167,12 @@ public void HookWithBadPathWithByteArrayTest() var hookResult = TestProcessMemory!.Hook(new PointerPath("bad pointer path"), new byte[5], new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsFailure, Is.True); - Assert.That(hookResult.Error, Is.TypeOf()); - Assert.That(((HookFailureOnPathEvaluation)hookResult.Error).Details, Is.Not.Null); } /// /// Tests the /// method. - /// The method is called with a pointer path that does not evaluate to a valid address. - /// Expects a result. + /// The method is called with a pointer path that does not evaluate to a valid address. Expects a failure. /// [Test] public void HookWithBadPathWithAssemblerTest() @@ -184,14 +180,12 @@ public void HookWithBadPathWithAssemblerTest() var hookResult = TestProcessMemory!.Hook(new PointerPath("bad pointer path"), AssembleAlternativeMovInt(), new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsFailure, Is.True); - Assert.That(hookResult.Error, Is.TypeOf()); - Assert.That(((HookFailureOnPathEvaluation)hookResult.Error).Details, Is.Not.Null); } /// /// Tests the method. /// The hook is called with an empty code byte array. - /// Expects a result. + /// Expects a result. /// [Test] public void HookWithEmptyCodeArrayTest() @@ -199,13 +193,13 @@ public void HookWithEmptyCodeArrayTest() var hookResult = TestProcessMemory!.Hook(FindMovIntAddress(), [], new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsFailure, Is.True); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests the method. /// The hook is called with an assembler that does not have any instructions. - /// Expects a result. + /// Expects a result. /// [Test] public void HookWithEmptyAssemblerTest() @@ -214,12 +208,12 @@ public void HookWithEmptyAssemblerTest() var hookResult = TestProcessMemory!.Hook(FindMovIntAddress(), assembler, new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsFailure, Is.True); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests with a - /// detached process. Expects a result. + /// detached process. Expects a result. /// [Test] public void HookWithDetachedProcessTest() @@ -230,12 +224,12 @@ public void HookWithDetachedProcessTest() var hookResult = TestProcessMemory!.Hook(0x1234, assembler, new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests with a - /// detached process. Expects a result. + /// detached process. Expects a result. /// [Test] public void HookWithPointerPathWithDetachedProcessTest() @@ -246,12 +240,12 @@ public void HookWithPointerPathWithDetachedProcessTest() var hookResult = TestProcessMemory!.Hook("1234", assembler, new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests with a - /// detached process. Expects a result. + /// detached process. Expects a result. /// [Test] public void HookWithByteArrayWithDetachedProcessTest() @@ -260,12 +254,12 @@ public void HookWithByteArrayWithDetachedProcessTest() var hookResult = TestProcessMemory!.Hook(0x1234, [0xCC], new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests with a - /// detached process. Expects a result. + /// detached process. Expects a result. /// [Test] public void HookWithByteArrayWithPointerPathWithDetachedProcessTest() @@ -274,7 +268,7 @@ public void HookWithByteArrayWithPointerPathWithDetachedProcessTest() var hookResult = TestProcessMemory!.Hook("1234", [0xCC], new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// @@ -408,8 +402,7 @@ public void InsertCodeAtWithAssemblerWithPointerPathTest() /// Tests /// . - /// Specifies a pointer path that does not evaluate to a valid address. - /// Expects a result. + /// Specifies a pointer path that does not evaluate to a valid address. Expects a failure. /// [Test] public void InsertCodeAtWithByteArrayWithBadPointerTest() @@ -417,25 +410,22 @@ public void InsertCodeAtWithByteArrayWithBadPointerTest() var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; var hookResult = TestProcessMemory!.InsertCodeAt("bad pointer path", bytes); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); } /// Tests /// . - /// Specifies a pointer path that does not evaluate to a valid address. - /// Expects a result. + /// Specifies a pointer path that does not evaluate to a valid address. Expects a failure. /// [Test] public void InsertCodeAtWithAssemblerWithBadPointerTest() { var hookResult = TestProcessMemory!.InsertCodeAt("bad pointer path", AssembleAlternativeMovInt()); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); } /// /// Tests . - /// Specifies an address of 0. Expects a result. + /// Specifies an address of 0. Expects a result. /// [Test] public void InsertCodeAtWithByteArrayWithZeroPointerTest() @@ -443,25 +433,25 @@ public void InsertCodeAtWithByteArrayWithZeroPointerTest() var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; var hookResult = TestProcessMemory!.InsertCodeAt(0, bytes); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests . - /// Specifies an address of 0. Expects a result. + /// Specifies an address of 0. Expects a result. /// [Test] public void InsertCodeAtWithAssemblerWithZeroPointerTest() { var hookResult = TestProcessMemory!.InsertCodeAt(0, AssembleAlternativeMovInt()); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests . /// Disposes the process memory instance and then call the method with valid parameters. - /// Expects a result. + /// Expects a result. /// [Test] public void InsertCodeAtWithByteArrayWithAddressWithDisposedInstanceTest() @@ -470,13 +460,13 @@ public void InsertCodeAtWithByteArrayWithAddressWithDisposedInstanceTest() var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress(), bytes); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests . /// Disposes the process memory instance and then call the method with valid parameters. - /// Expects a result. + /// Expects a result. /// [Test] public void InsertCodeAtWithByteArrayWithPointerPathWithDisposedInstanceTest() @@ -485,13 +475,13 @@ public void InsertCodeAtWithByteArrayWithPointerPathWithDisposedInstanceTest() var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress().ToString("X"), bytes); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests . /// Disposes the process memory instance and then call the method with valid parameters. - /// Expects a result. + /// Expects a result. /// [Test] public void InsertCodeAtWithAssemblerWithAddressWithDisposedInstanceTest() @@ -499,13 +489,13 @@ public void InsertCodeAtWithAssemblerWithAddressWithDisposedInstanceTest() TestProcessMemory!.Dispose(); var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress(), AssembleAlternativeMovInt()); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// Tests /// . /// Disposes the process memory instance and then call the method with valid parameters. - /// Expects a result. + /// Expects a result. /// [Test] public void InsertCodeAtWithAssemblerWithPointerPathWithDisposedInstanceTest() @@ -514,7 +504,7 @@ public void InsertCodeAtWithAssemblerWithPointerPathWithDisposedInstanceTest() var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress().ToString("X"), AssembleAlternativeMovInt()); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } #endregion @@ -659,7 +649,7 @@ public void ReplaceCodeAtWithAssemblerWithPointerPathTest() /// Tests /// . /// Use a pointer path that does not evaluate to a valid address, but otherwise valid parameters. - /// Expects the result to be a . + /// Expects a failure. /// [Test] public void ReplaceCodeAtWithByteArrayWithBadPointerPathTest() @@ -667,7 +657,6 @@ public void ReplaceCodeAtWithByteArrayWithBadPointerPathTest() var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; var hookResult = TestProcessMemory!.ReplaceCodeAt("bad pointer path", 1, bytes); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); } /// Tests @@ -680,39 +669,38 @@ public void ReplaceCodeAtWithAssemblerWithBadPointerPathTest() { var hookResult = TestProcessMemory!.ReplaceCodeAt("bad pointer path", 1, AssembleAlternativeMovInt()); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); } /// Tests /// . /// Specify an empty code array, but otherwise valid parameters. - /// Expects the result to be a . + /// Expects the result to be a . /// [Test] public void ReplaceCodeAtWithByteArrayWithNoCodeTest() { var hookResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 1, []); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// Tests /// . /// Specify an empty assembler, but otherwise valid parameters. - /// Expects the result to be a . + /// Expects the result to be a . /// [Test] public void ReplaceCodeAtWithAssemblerWithNoCodeTest() { var hookResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 1, new Assembler(Is64Bit ? 64 : 32)); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// Tests /// . /// Specifies a number of instructions to replace of 0, but otherwise valid parameters. - /// Expects the result to be a . + /// Expects the result to be a . /// [Test] public void ReplaceCodeAtWithByteArrayWithZeroInstructionTest() @@ -720,7 +708,7 @@ public void ReplaceCodeAtWithByteArrayWithZeroInstructionTest() var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; var hookResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 0, bytes); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// Tests @@ -733,7 +721,7 @@ public void ReplaceCodeAtWithAssemblerWithZeroInstructionTest() { var hookResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 0, AssembleAlternativeMovInt()); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } #endregion @@ -766,7 +754,7 @@ protected override Assembler AssembleRcxMov(uint value) /// /// Tests with a 64-bit - /// address on a 32-bit process. Expects a result. + /// address on a 32-bit process. Expects a result. /// [Test] public void HookWithByteArrayOnX64AddressOnX86ProcessTest() @@ -776,12 +764,12 @@ public void HookWithByteArrayOnX64AddressOnX86ProcessTest() var hookResult = TestProcessMemory!.Hook((UIntPtr)address, bytes, new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests with a 64-bit - /// address on a 32-bit process. Expects a result. + /// address on a 32-bit process. Expects a result. /// [Test] public void HookWithAssemblerOnX64AddressOnX86ProcessTest() @@ -790,12 +778,12 @@ public void HookWithAssemblerOnX64AddressOnX86ProcessTest() var hookResult = TestProcessMemory!.Hook((UIntPtr)address, AssembleAlternativeMovInt(), new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests with a - /// 64-bit address on a 32-bit process. Expects a result. + /// 64-bit address on a 32-bit process. Expects a result. /// [Test] public void InsertCodeAtWithByteArrayOnX64AddressOnX86ProcessTest() @@ -804,12 +792,12 @@ public void InsertCodeAtWithByteArrayOnX64AddressOnX86ProcessTest() var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; var hookResult = TestProcessMemory!.InsertCodeAt((UIntPtr)address, bytes); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests - /// with a 64-bit address on a 32-bit process. Expects a result. + /// with a 64-bit address on a 32-bit process. Expects a result. /// [Test] public void InsertCodeAtWithAssemblerOnX64AddressOnX86ProcessTest() @@ -817,12 +805,12 @@ public void InsertCodeAtWithAssemblerOnX64AddressOnX86ProcessTest() var address = (ulong)uint.MaxValue + 1; var hookResult = TestProcessMemory!.InsertCodeAt((UIntPtr)address, AssembleAlternativeMovInt()); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests - /// with a 64-bit address on a 32-bit process. Expects a result. + /// with a 64-bit address on a 32-bit process. Expects a result. /// [Test] public void ReplaceCodeAtWithByteArrayOnX64AddressOnX86ProcessTest() @@ -831,13 +819,13 @@ public void ReplaceCodeAtWithByteArrayOnX64AddressOnX86ProcessTest() var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; var hookResult = TestProcessMemory!.ReplaceCodeAt((UIntPtr)address, 1, bytes); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } /// /// Tests /// - /// with a 64-bit address on a 32-bit process. Expects a result. + /// with a 64-bit address on a 32-bit process. Expects a result. /// [Test] public void ReplaceCodeAtWithAssemblerOnX64AddressOnX86ProcessTest() @@ -845,6 +833,6 @@ public void ReplaceCodeAtWithAssemblerOnX64AddressOnX86ProcessTest() var address = (ulong)uint.MaxValue + 1; var hookResult = TestProcessMemory!.ReplaceCodeAt((UIntPtr)address, 1, AssembleAlternativeMovInt()); Assert.That(hookResult.IsSuccess, Is.False); - Assert.That(hookResult.Error, Is.TypeOf()); + Assert.That(hookResult.Failure, Is.TypeOf()); } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index 18a0c87..e8c657c 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -91,19 +91,19 @@ public void AllocateNearAddressTest() /// /// Tests the method with a zero size. - /// This should return an . + /// This should return an . /// [Test] public void AllocateZeroTest() { var allocateResult = TestProcessMemory!.Allocate(0, false); Assert.That(allocateResult.IsSuccess, Is.False); - Assert.That(allocateResult.Error, Is.InstanceOf()); + Assert.That(allocateResult.Failure, Is.InstanceOf()); } /// /// Tests the method with a detached process. - /// This should return an . + /// This should return an . /// [Test] public void AllocateWithDetachedProcessTest() @@ -111,7 +111,7 @@ public void AllocateWithDetachedProcessTest() TestProcessMemory!.Dispose(); var allocateResult = TestProcessMemory.Allocate(0x1000, false); Assert.That(allocateResult.IsSuccess, Is.False); - Assert.That(allocateResult.Error, Is.InstanceOf()); + Assert.That(allocateResult.Failure, Is.InstanceOf()); } /// @@ -259,19 +259,19 @@ public void ReserveWithNearAddressTest() /// /// Tests the method with a size of zero. - /// Expects the result to be an . + /// Expects the result to be an . /// [Test] public void ReserveZeroTest() { var reserveResult = TestProcessMemory!.Reserve(0, false); Assert.That(reserveResult.IsSuccess, Is.False); - Assert.That(reserveResult.Error, Is.InstanceOf()); + Assert.That(reserveResult.Failure, Is.InstanceOf()); } /// /// Tests the method with a detached process. - /// Expects the result to be an . + /// Expects the result to be an . /// [Test] public void ReserveWithDetachedProcessTest() @@ -279,7 +279,7 @@ public void ReserveWithDetachedProcessTest() TestProcessMemory!.Dispose(); var reserveResult = TestProcessMemory.Reserve(0x1000, false); Assert.That(reserveResult.IsSuccess, Is.False); - Assert.That(reserveResult.Error, Is.InstanceOf()); + Assert.That(reserveResult.Failure, Is.InstanceOf()); } /// @@ -342,7 +342,7 @@ public void StoreWithoutPreAllocationWithMultipleSmallValuesTest() var reservations = reservationResults.Select(r => r.Value).ToList(); var readBackValues = reservations.Select(r => TestProcessMemory!.ReadBytes(r.Range.Start, value.Length) - .GetValueOrDefault()); + .ValueOrDefault()); // The store method should have allocated only one range that's big enough to accomodate all the values. Assert.That(TestProcessMemory!.Allocations, Has.Count.EqualTo(1)); @@ -383,7 +383,7 @@ public void StoreWithMultipleOverflowingValuesTest() /// /// Tests the method with a detached process. - /// Expects the result to be an . + /// Expects the result to be an . /// [Test] public void StoreWithDetachedProcessTest() @@ -391,12 +391,12 @@ public void StoreWithDetachedProcessTest() TestProcessMemory!.Dispose(); var storeResult = TestProcessMemory.Store(new byte[8]); Assert.That(storeResult.IsSuccess, Is.False); - Assert.That(storeResult.Error, Is.InstanceOf()); + Assert.That(storeResult.Failure, Is.InstanceOf()); } /// /// Tests the method with a detached process. - /// Expects the result to be an . + /// Expects the result to be an . /// [Test] public void StoreWithAllocationWithDetachedProcessTest() @@ -405,7 +405,7 @@ public void StoreWithAllocationWithDetachedProcessTest() TestProcessMemory.Dispose(); var storeResult = TestProcessMemory.Store(new byte[8], allocation); Assert.That(storeResult.IsSuccess, Is.False); - Assert.That(storeResult.Error, Is.InstanceOf()); + Assert.That(storeResult.Failure, Is.InstanceOf()); } /// @@ -460,7 +460,7 @@ public void StoreStringWithAllocationTest() /// /// Tests the method. - /// Specify invalid settings. The result is expected to be an . + /// Specify invalid settings. The result is expected to be an . /// [Test] public void StoreStringWithoutAllocationWithInvalidSettingsTest() @@ -469,13 +469,13 @@ public void StoreStringWithoutAllocationWithInvalidSettingsTest() var result = TestProcessMemory!.StoreString("Hello world", invalidSettings); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } /// /// Tests the method. /// Specify valid settings, but with a length prefix that is too short to store the provided string. The result is - /// expected to be an . + /// expected to be an . /// [Test] public void StoreStringWithoutAllocationWithIncompatibleSettingsTest() @@ -485,12 +485,12 @@ public void StoreStringWithoutAllocationWithIncompatibleSettingsTest() var result = TestProcessMemory!.StoreString(new string('a', 256), settingsThatCanOnlyStoreUpTo255Chars); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } /// /// Tests the method. - /// Specify invalid settings. The result is expected to be an . + /// Specify invalid settings. The result is expected to be an . /// [Test] public void StoreStringWithAllocationWithInvalidSettingsTest() @@ -500,13 +500,13 @@ public void StoreStringWithAllocationWithInvalidSettingsTest() var result = TestProcessMemory!.StoreString("Hello world", invalidSettings, allocation); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } /// /// Tests the method. /// Specify valid settings, but with a length prefix that is too short to store the provided string. The result is - /// expected to be an . + /// expected to be an . /// [Test] public void StoreStringWithAllocationWithIncompatibleSettingsTest() @@ -518,12 +518,12 @@ public void StoreStringWithAllocationWithIncompatibleSettingsTest() allocation); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } /// /// Tests the method with a detached process. - /// Expects the result to be an . + /// Expects the result to be an . /// [Test] public void StoreStringWithDetachedProcessTest() @@ -531,12 +531,12 @@ public void StoreStringWithDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.StoreString("Hello world", GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } /// /// Tests the method with a detached - /// process. Expects the result to be an . + /// process. Expects the result to be an . /// [Test] public void StoreStringWithAllocationWithDetachedProcessTest() @@ -545,7 +545,7 @@ public void StoreStringWithAllocationWithDetachedProcessTest() TestProcessMemory.Dispose(); var result = TestProcessMemory.StoreString("Hello world", GetDotNetStringSettings(), allocation); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } } diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs index b92832c..2c3c761 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs @@ -55,7 +55,7 @@ public void WriteIntTest() /// /// Tests with an address of 1, which is not readable. - /// When trying to read the value, the result should be a . + /// When trying to read the value, the result should be a . /// [Test] public void ReadIntWithOutOfRangeAddressTest() @@ -63,12 +63,12 @@ public void ReadIntWithOutOfRangeAddressTest() var anchor = TestProcessMemory!.GetAnchor(1); var readResult = anchor.Read(); Assert.That(readResult.IsSuccess, Is.False); - Assert.That(readResult.Error, Is.InstanceOf()); + Assert.That(readResult.Failure, Is.InstanceOf()); } /// /// Tests with an address of 1, which is not writeable. - /// When trying to write the value, the result should be a . + /// When trying to write the value, the result should be a . /// [Test] public void WriteIntWithOutOfRangeAddressTest() @@ -76,7 +76,7 @@ public void WriteIntWithOutOfRangeAddressTest() var anchor = TestProcessMemory!.GetAnchor(1); var writeResult = anchor.Write(1234567); Assert.That(writeResult.IsSuccess, Is.False); - Assert.That(writeResult.Error, Is.InstanceOf()); + Assert.That(writeResult.Failure, Is.InstanceOf()); } /// @@ -112,14 +112,13 @@ public void FreezeIntAndDisposeFreezerTest() } /// - /// Tests (the thread-based freezer - /// implementation). + /// Tests (the thread-based freezer implementation). /// [Test] public void FreezeIntThreadTest() { var anchor = TestProcessMemory!.GetAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); - using var freezer = new ThreadValueFreezer(anchor, 1234567); + using var freezer = new ThreadValueFreezer(anchor, 1234567); ProceedToNextStep(); Thread.Sleep(100); // Make sure the freezer has time to write the value to make the test consistent ProceedToNextStep(); @@ -127,19 +126,37 @@ public void FreezeIntThreadTest() } /// - /// Tests the Dispose method of (the - /// thread-based freezer implementation). + /// Tests the Dispose method of (the thread-based freezer implementation). /// [Test] public void FreezeIntThreadAndDisposeTest() { var anchor = TestProcessMemory!.GetAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); - var freezer = new ThreadValueFreezer(anchor, 1234567); + var freezer = new ThreadValueFreezer(anchor, 1234567); freezer.Dispose(); ProceedToNextStep(); ProceedToNextStep(); AssertExpectedFinalResults(); } + + /// + /// Freezes an unreadable value, and check that the freezer raises the + /// event. + /// + [Test] + public void FreezeFailureTest() + { + var anchor = TestProcessMemory!.GetAnchor(1); + using var freezer = anchor.Freeze(1234567); + + List failures = []; + freezer.FreezeFailed += (_, args) => + { + failures.Add(args.Failure); + }; + Thread.Sleep(3000); + Assert.That(failures, Has.Count.GreaterThan(0)); + } #endregion diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs index 455ce66..97433e7 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs @@ -12,26 +12,26 @@ public class ProcessMemoryInstancelessAttachTest { /// /// Tests when no process with the given name is found. - /// Expects a result. + /// Expects a result. /// [Test] public void OpenProcessByNameWithNoMatchTest() { var result = ProcessMemory.OpenProcessByName("ThisProcessDoesNotExist"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } /// /// Tests with a PID that does not match any running process. - /// Expects a result. + /// Expects a result. /// [Test] public void OpenProcessByInvalidPidTest() { var result = ProcessMemory.OpenProcessById(-1); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } } diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs index 717f821..30f3554 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs @@ -23,11 +23,23 @@ public void EvaluateOnKnownPointerTest() Assert.That(result.Value, Is.EqualTo(GetMaxPointerValue())); } + /// + /// Tests an error case where the pointer path has a correct module name. + /// + [Test] + public void EvaluateWithModuleTest() + { + var result = TestProcessMemory!.EvaluateMemoryAddress($"{MainModuleName}+8"); + + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Value, Is.Not.EqualTo(UIntPtr.Zero)); + } + /// /// Tests an error case where the pointer path points to a value located after the last possible byte in memory /// (the maximum value of a UIntPtr + 1). - /// The operation is expected to fail with a on an x64 - /// process, or a on an x86 process. + /// The operation is expected to fail with a on an x64 + /// process, or a on an x86 process. /// [Test] public void EvaluateOverMaxPointerValueTest() @@ -35,23 +47,23 @@ public void EvaluateOverMaxPointerValueTest() var result = TestProcessMemory!.EvaluateMemoryAddress(GetPathToPointerToMaxAddressPlusOne()); Assert.That(result.IsSuccess, Is.False); - var error = result.Error; + var failure = result.Failure; if (Is64Bit) { - Assert.That(error, Is.TypeOf()); - var pathError = (PathEvaluationFailureOnPointerOutOfRange)error; + Assert.That(failure, Is.TypeOf()); + var pathError = (PointerOutOfRangeFailure)failure; Assert.That(pathError.Offset, Is.EqualTo(new PointerOffset(1, false))); Assert.That(pathError.PreviousAddress, Is.EqualTo(UIntPtr.MaxValue)); } else - Assert.That(error, Is.TypeOf()); + Assert.That(failure, Is.TypeOf()); } /// /// Tests an error case where the pointer path points to an unreachable value because one of the pointers in the /// path (but not the last one) points to an unreadable address. - /// The operation is expected to fail with a . + /// The operation is expected to fail with a failure. /// [Test] public void EvaluateWithUnreadableAddressHalfwayThroughTest() @@ -59,21 +71,16 @@ public void EvaluateWithUnreadableAddressHalfwayThroughTest() var result = TestProcessMemory!.EvaluateMemoryAddress(GetPathToPointerThroughMaxAddress()); Assert.That(result.IsSuccess, Is.False); - var error = result.Error; - Assert.That(error, Is.TypeOf()); - var pathError = (PathEvaluationFailureOnPointerReadFailure)error; - Assert.That(pathError.Address, Is.EqualTo(GetMaxPointerValue())); - Assert.That(pathError.Details, Is.TypeOf()); - var readFailure = (ReadFailureOnSystemRead)pathError.Details; - Assert.That(readFailure.Details, Is.TypeOf()); - var osFailure = (OperatingSystemCallFailure)readFailure.Details; + var failure = result.Failure; + Assert.That(failure, Is.TypeOf()); + var osFailure = (OperatingSystemCallFailure)failure; Assert.That(osFailure.ErrorCode, Is.GreaterThan(0)); - Assert.That(osFailure.ErrorMessage, Is.Not.Empty); + Assert.That(osFailure.Message, Is.Not.Empty); } /// /// Tests an error case where the pointer path given to a read operation points to zero. - /// The operation is expected to fail with a . + /// The operation is expected to fail with a . /// [Test] public void EvaluateOnZeroPointerTest() @@ -81,16 +88,16 @@ public void EvaluateOnZeroPointerTest() var result = TestProcessMemory!.EvaluateMemoryAddress("0"); Assert.That(result.IsSuccess, Is.False); - var error = result.Error; - Assert.That(error, Is.TypeOf()); - var pathError = (PathEvaluationFailureOnPointerOutOfRange)error; + var failure = result.Failure; + Assert.That(failure, Is.TypeOf()); + var pathError = (PointerOutOfRangeFailure)failure; Assert.That(pathError.Offset, Is.EqualTo(PointerOffset.Zero)); Assert.That(pathError.PreviousAddress, Is.EqualTo(UIntPtr.Zero)); } /// /// Tests an error case where the pointer path has a module that is not part of the target process. - /// The operation is expected to fail with a . + /// The operation is expected to fail with a . /// [Test] public void EvaluateWithUnknownModuleTest() @@ -98,14 +105,14 @@ public void EvaluateWithUnknownModuleTest() var result = TestProcessMemory!.EvaluateMemoryAddress("ThisModuleDoesNotExist.dll+10,10"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); - var error = (PathEvaluationFailureOnBaseModuleNotFound)result.Error; - Assert.That(error.ModuleName, Is.EqualTo("ThisModuleDoesNotExist.dll")); + Assert.That(result.Failure, Is.TypeOf()); + var failure = (BaseModuleNotFoundFailure)result.Failure; + Assert.That(failure.ModuleName, Is.EqualTo("ThisModuleDoesNotExist.dll")); } /// /// Tests with a detached process. - /// The operation is expected to fail with a . + /// The operation is expected to fail with a . /// [Test] public void EvaluateWithDetachedProcessTest() @@ -113,7 +120,7 @@ public void EvaluateWithDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory!.EvaluateMemoryAddress(GetPathToMaxAddress()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); + Assert.That(result.Failure, Is.TypeOf()); } } @@ -129,26 +136,26 @@ public class ProcessMemoryEvaluateTestX86 : ProcessMemoryEvaluateTest /// /// Tests with a pointer path that starts with an address that is /// not within the 32-bit address space. - /// Expect a . + /// Expect a . /// [Test] public void EvaluateWithX64PathOnX86ProcessTest() { var result = TestProcessMemory!.EvaluateMemoryAddress("1000000000,4"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); + Assert.That(result.Failure, Is.TypeOf()); } /// /// Tests with a pointer path that starts at a module address, /// with the maximum 32-bit offset added to it. - /// Expect a . + /// Expect a . /// [Test] public void EvaluateWithX64ModuleOffsetOnX86ProcessTest() { var result = TestProcessMemory!.EvaluateMemoryAddress($"{MainModuleName}+FFFFFFFF"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); + Assert.That(result.Failure, Is.TypeOf()); } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs index d751099..da16465 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs @@ -43,7 +43,7 @@ public void SetUp() public void InjectLibraryTest() { var result = TestProcessMemory!.InjectLibrary(GetInjectedLibraryPath()); - Assert.That(result.IsSuccess, Is.True, () => result.Error.ToString()); + Assert.That(result.IsSuccess, Is.True, () => result.Failure.ToString()); var output = ProceedToNextStep(); Assert.That(output, Is.EqualTo("Injected library attached")); } @@ -70,7 +70,7 @@ public void InjectLibraryWithNonAsciiPathTest() /// /// Tests the method. /// Specify a path to a non-existent library file. - /// The method should fail with a . + /// The method should fail with a . /// [Test] public void InjectLibraryWithLibraryFileNotFoundTest() @@ -78,14 +78,15 @@ public void InjectLibraryWithLibraryFileNotFoundTest() const string path = "./NonExistentLibrary.dll"; var result = TestProcessMemory!.InjectLibrary(path); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf()); - var error = (InjectionFailureOnLibraryFileNotFound)result.Error; - Assert.That(error.LibraryPath, Has.Length.GreaterThan(path.Length)); // We expect a full path - Assert.That(error.LibraryPath, Does.EndWith("NonExistentLibrary.dll")); + Assert.That(result.Failure, Is.TypeOf()); + var failure = (LibraryFileNotFoundFailure)result.Failure; + Assert.That(failure.LibraryPath, Has.Length.GreaterThan(path.Length)); // We expect a full path + Assert.That(failure.LibraryPath, Does.EndWith("NonExistentLibrary.dll")); } /// - /// Tests the method. + /// Tests the method with a detached process. + /// The method should fail with a . /// [Test] public void InjectLibraryWithDetachedProcessTest() @@ -93,7 +94,7 @@ public void InjectLibraryWithDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.InjectLibrary(GetInjectedLibraryPath()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } } @@ -108,13 +109,13 @@ public class ProcessMemoryInjectionTestX86 : ProcessMemoryInjectionTest /// /// Tests with a 64-bit library on a 32-bit process. - /// Expect a . + /// Expect a . /// [Test] public void InjectX64LibraryOnX86ProcessTest() { var result = TestProcessMemory!.InjectLibrary(GetInjectedLibraryPath(true)); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index 6bce5cc..80fb7a0 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -23,51 +23,48 @@ public class ProcessMemoryReadTest : BaseProcessMemoryTest public void ReadBytesTest() { PointerPath pointerPath = GetPointerPathForValueAtIndex(IndexOfOutputByteArray); - Assert.That(TestProcessMemory!.ReadBytes(pointerPath, 4).GetValueOrDefault(), + Assert.That(TestProcessMemory!.ReadBytes(pointerPath, 4).ValueOrDefault(), Is.EqualTo(new byte[] { 0x11, 0x22, 0x33, 0x44 })); ProceedToNextStep(); - Assert.That(TestProcessMemory.ReadBytes(pointerPath, 4).GetValueOrDefault(), + Assert.That(TestProcessMemory.ReadBytes(pointerPath, 4).ValueOrDefault(), Is.EqualTo(new byte[] { 0x55, 0x66, 0x77, 0x88 })); } /// /// Tests with a zero pointer. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadBytesWithZeroPointerTest() { var result = TestProcessMemory!.ReadBytes(UIntPtr.Zero, 4); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnZeroPointer))); + Assert.That(result.Failure, Is.TypeOf(typeof(ZeroPointerFailure))); } /// /// Tests with an unreadable address. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadBytesWithUnreadableAddressTest() { var result = TestProcessMemory!.ReadBytes(GetMaxPointerValue(), 1); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); - var systemError = ((ReadFailureOnSystemRead)result.Error).Details; + Assert.That(result.Failure, Is.TypeOf(typeof(OperatingSystemCallFailure))); + var systemError = (OperatingSystemCallFailure)result.Failure; Assert.That(systemError, Is.Not.Null); } /// /// Tests with a bad pointer path. - /// Expect the result to be a . + /// Expect a failure. /// [Test] public void ReadBytesWithInvalidPathTest() { var result = TestProcessMemory!.ReadBytes("bad pointer path", 4); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); - var pathError = ((ReadFailureOnPointerPathEvaluation)result.Error).Details; - Assert.That(pathError, Is.Not.Null); } /// @@ -84,20 +81,20 @@ public void ReadBytesWithZeroLengthTest() /// /// Tests with a negative length. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadBytesWithNegativeLengthTest() { var result = TestProcessMemory!.ReadBytes(OuterClassPointer, -4); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnInvalidArguments))); + Assert.That(result.Failure, Is.TypeOf(typeof(InvalidArgumentFailure))); } /// /// Tests with an address and length that would make the read /// start on a readable address but end on an unreadable one. - /// Expect the result to be a . + /// Expect the result to be a . /// /// This is what is for. [Test] @@ -111,12 +108,12 @@ public void ReadBytesOnPartiallyUnreadableRangeTest() // one. var result = TestProcessMemory.ReadBytes(targetAddress, 8); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); + Assert.That(result.Failure, Is.TypeOf(typeof(OperatingSystemCallFailure))); } /// /// Tests with a detached process. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadBytesOnDetachedProcessTest() @@ -124,12 +121,12 @@ public void ReadBytesOnDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.ReadBytes(OuterClassPointer, 4); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } /// /// Tests with a detached process. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadBytesWithPointerPathOnDetachedProcessTest() @@ -137,7 +134,7 @@ public void ReadBytesWithPointerPathOnDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.ReadBytes(OuterClassPointer.ToString("X"), 4); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } #endregion @@ -170,42 +167,39 @@ public void ReadBytesPartialTest() /// /// Tests with a zero pointer. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadBytesPartialWithZeroPointerTest() { var result = TestProcessMemory!.ReadBytesPartial(UIntPtr.Zero, new byte[4], 4); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnZeroPointer))); + Assert.That(result.Failure, Is.TypeOf(typeof(ZeroPointerFailure))); } /// /// Tests with an unreadable address. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadBytesPartialWithUnreadableAddressTest() { - var result = TestProcessMemory!.ReadBytesPartial(GetMaxPointerValue(), new byte[1], 1); + var result = TestProcessMemory!.ReadBytesPartial(1, new byte[1], 1); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); - var systemError = ((ReadFailureOnSystemRead)result.Error).Details; + Assert.That(result.Failure, Is.TypeOf(typeof(OperatingSystemCallFailure))); + var systemError = (OperatingSystemCallFailure)result.Failure; Assert.That(systemError, Is.Not.Null); } /// /// Tests with a bad pointer path. - /// Expect the result to be a . + /// Expect a failure. /// [Test] public void ReadBytesPartialWithInvalidPathTest() { var result = TestProcessMemory!.ReadBytesPartial("bad pointer path", new byte[4], 4); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); - var pathError = ((ReadFailureOnPointerPathEvaluation)result.Error).Details; - Assert.That(pathError, Is.Not.Null); } /// @@ -223,14 +217,14 @@ public void ReadBytesPartialWithZeroLengthTest() /// /// Tests with a length exceeding the buffer /// capacity. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadBytesPartialWithExcessiveLengthTest() { var result = TestProcessMemory!.ReadBytesPartial(OuterClassPointer, new byte[4], 8); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnInvalidArguments))); + Assert.That(result.Failure, Is.TypeOf(typeof(InvalidArgumentFailure))); } /// @@ -260,7 +254,7 @@ public void ReadBytesPartialOnPartiallyUnreadableRangeTest() /// /// Tests with a detached process. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadBytesPartialOnDetachedProcessTest() @@ -268,12 +262,12 @@ public void ReadBytesPartialOnDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.ReadBytesPartial(OuterClassPointer, new byte[4], 4); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } /// /// Tests with a detached process. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadBytesPartialWithPointerPathOnDetachedProcessTest() @@ -281,7 +275,7 @@ public void ReadBytesPartialWithPointerPathOnDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.ReadBytesPartial(OuterClassPointer.ToString("X"), new byte[4], 4); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } #endregion @@ -329,9 +323,9 @@ public void ReadTwoStepGenericTest(Type targetType, int valueIndexInOutput, obje public void ReadBoolTest() { var address = GetAddressForValueAtIndex(IndexOfOutputBool); - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialBoolValue)); + Assert.That(TestProcessMemory!.Read(address).ValueOrDefault(), Is.EqualTo(InitialBoolValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalBoolValue)); + Assert.That(TestProcessMemory.Read(address).ValueOrDefault(), Is.EqualTo(ExpectedFinalBoolValue)); } /// @@ -343,9 +337,9 @@ public void ReadBoolTest() public void ReadByteTest() { var address = GetAddressForValueAtIndex(IndexOfOutputByte); - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialByteValue)); + Assert.That(TestProcessMemory!.Read(address).ValueOrDefault(), Is.EqualTo(InitialByteValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalByteValue)); + Assert.That(TestProcessMemory.Read(address).ValueOrDefault(), Is.EqualTo(ExpectedFinalByteValue)); } /// @@ -357,9 +351,9 @@ public void ReadByteTest() public void ReadShortTest() { var address = GetAddressForValueAtIndex(IndexOfOutputShort); - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialShortValue)); + Assert.That(TestProcessMemory!.Read(address).ValueOrDefault(), Is.EqualTo(InitialShortValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalShortValue)); + Assert.That(TestProcessMemory.Read(address).ValueOrDefault(), Is.EqualTo(ExpectedFinalShortValue)); } /// @@ -371,9 +365,9 @@ public void ReadShortTest() public void ReadUShortTest() { var address = GetAddressForValueAtIndex(IndexOfOutputUShort); - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialUShortValue)); + Assert.That(TestProcessMemory!.Read(address).ValueOrDefault(), Is.EqualTo(InitialUShortValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalUShortValue)); + Assert.That(TestProcessMemory.Read(address).ValueOrDefault(), Is.EqualTo(ExpectedFinalUShortValue)); } /// @@ -385,9 +379,9 @@ public void ReadUShortTest() public void ReadIntTest() { var address = GetAddressForValueAtIndex(IndexOfOutputInt); - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialIntValue)); + Assert.That(TestProcessMemory!.Read(address).ValueOrDefault(), Is.EqualTo(InitialIntValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalIntValue)); + Assert.That(TestProcessMemory.Read(address).ValueOrDefault(), Is.EqualTo(ExpectedFinalIntValue)); } /// @@ -399,9 +393,9 @@ public void ReadIntTest() public void ReadUIntTest() { var address = GetAddressForValueAtIndex(IndexOfOutputUInt); - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialUIntValue)); + Assert.That(TestProcessMemory!.Read(address).ValueOrDefault(), Is.EqualTo(InitialUIntValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalUIntValue)); + Assert.That(TestProcessMemory.Read(address).ValueOrDefault(), Is.EqualTo(ExpectedFinalUIntValue)); } /// @@ -413,9 +407,9 @@ public void ReadUIntTest() public void ReadLongTest() { var address = GetAddressForValueAtIndex(IndexOfOutputLong); - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialLongValue)); + Assert.That(TestProcessMemory!.Read(address).ValueOrDefault(), Is.EqualTo(InitialLongValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalLongValue)); + Assert.That(TestProcessMemory.Read(address).ValueOrDefault(), Is.EqualTo(ExpectedFinalLongValue)); } /// @@ -427,9 +421,9 @@ public void ReadLongTest() public void ReadULongTest() { var address = GetAddressForValueAtIndex(IndexOfOutputULong); - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialULongValue)); + Assert.That(TestProcessMemory!.Read(address).ValueOrDefault(), Is.EqualTo(InitialULongValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalULongValue)); + Assert.That(TestProcessMemory.Read(address).ValueOrDefault(), Is.EqualTo(ExpectedFinalULongValue)); } /// @@ -441,9 +435,9 @@ public void ReadULongTest() public void ReadFloatTest() { var address = GetAddressForValueAtIndex(IndexOfOutputFloat); - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialFloatValue)); + Assert.That(TestProcessMemory!.Read(address).ValueOrDefault(), Is.EqualTo(InitialFloatValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalFloatValue)); + Assert.That(TestProcessMemory.Read(address).ValueOrDefault(), Is.EqualTo(ExpectedFinalFloatValue)); } /// @@ -455,9 +449,9 @@ public void ReadFloatTest() public void ReadDoubleTest() { var address = GetAddressForValueAtIndex(IndexOfOutputDouble); - Assert.That(TestProcessMemory!.Read(address).GetValueOrDefault(), Is.EqualTo(InitialDoubleValue)); + Assert.That(TestProcessMemory!.Read(address).ValueOrDefault(), Is.EqualTo(InitialDoubleValue)); ProceedToNextStep(); - Assert.That(TestProcessMemory.Read(address).GetValueOrDefault(), Is.EqualTo(ExpectedFinalDoubleValue)); + Assert.That(TestProcessMemory.Read(address).ValueOrDefault(), Is.EqualTo(ExpectedFinalDoubleValue)); } /// @@ -468,7 +462,7 @@ public void ReadDoubleTest() /// [Test] public void ReadNestedLongTest() => Assert.That( - TestProcessMemory!.Read(GetPointerPathForValueAtIndex(IndexOfOutputInnerLong)).GetValueOrDefault(), + TestProcessMemory!.Read(GetPointerPathForValueAtIndex(IndexOfOutputInnerLong)).ValueOrDefault(), Is.EqualTo(InitialInnerLongValue)); /// @@ -481,69 +475,65 @@ public void ReadNestedLongTest() => Assert.That( [Test] public void ReadUIntPtrMaxValueTest() { - var ptr = TestProcessMemory!.Read(GetPathToPointerToMaxAddress()).GetValueOrDefault(); + var ptr = TestProcessMemory!.Read(GetPathToPointerToMaxAddress()).ValueOrDefault(); Assert.That(ptr, Is.EqualTo(GetMaxPointerValue())); } /// /// Tests an edge case where the pointer path given to a read operation points to the last possible byte in memory /// (the maximum value of a UIntPtr). - /// The read operation is expected to fail with a (this memory region is not - /// readable). + /// The read operation is expected to fail with a (this memory region is + /// not readable). /// [Test] public void ReadAtMaxPointerValueTest() { var result = TestProcessMemory!.Read(GetMaxPointerValue()); Assert.That(result.IsSuccess, Is.False); - var error = result.Error; - Assert.That(error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); - var systemError = ((ReadFailureOnSystemRead)error).Details; - Assert.That(systemError, Is.TypeOf(typeof(OperatingSystemCallFailure))); - var osError = (OperatingSystemCallFailure)systemError; - Assert.That(osError.ErrorCode, Is.GreaterThan(0)); - Assert.That(osError.ErrorMessage, Is.Not.Empty); + Assert.That(result.Failure, Is.TypeOf(typeof(OperatingSystemCallFailure))); + var systemError = (OperatingSystemCallFailure)result.Failure; + Assert.That(systemError.ErrorCode, Is.GreaterThan(0)); + Assert.That(systemError.Message, Is.Not.Empty); } /// /// Tests the method with a reference type. - /// It should fail with a . + /// It should fail with a . /// [Test] public void ReadIncompatibleTypeTest() { var result = TestProcessMemory!.Read(typeof(string), OuterClassPointer + 0x38); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnUnsupportedType))); + Assert.That(result.Failure, Is.TypeOf(typeof(UnsupportedTypeReadFailure))); } /// /// Tests the method with a pointer path that fails to evaluate. - /// It should fail with a . + /// It should fail. /// [Test] public void ReadWithBadPointerPathTest() { var result = TestProcessMemory!.Read("bad pointer path"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); } /// /// Tests the method with a zero pointer. - /// It should fail with a . + /// It should fail with a . /// [Test] public void ReadWithZeroPointerTest() { var result = TestProcessMemory!.Read(UIntPtr.Zero); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnZeroPointer))); + Assert.That(result.Failure, Is.TypeOf(typeof(ZeroPointerFailure))); } /// /// Tests the method with a detached process. - /// It should fail with a . + /// It should fail with a . /// [Test] public void ReadOnDetachedProcessTest() @@ -551,12 +541,12 @@ public void ReadOnDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.Read(OuterClassPointer); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } /// /// Tests the method with a detached process. - /// It should fail with a . + /// It should fail with a . /// [Test] public void ReadWithPointerPathOnDetachedProcessTest() @@ -564,36 +554,35 @@ public void ReadWithPointerPathOnDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.Read(OuterClassPointer.ToString("X")); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } /// /// Tests the method with a pointer path that fails to evaluate. - /// It should fail with a . + /// It should fail. /// [Test] public void ReadObjectWithBadPointerPathTest() { var result = TestProcessMemory!.Read(typeof(long), "bad pointer path"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); } /// /// Tests the method with a zero pointer. - /// It should fail with a . + /// It should fail with a . /// [Test] public void ReadObjectWithZeroPointerTest() { var result = TestProcessMemory!.Read(typeof(long), UIntPtr.Zero); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnZeroPointer))); + Assert.That(result.Failure, Is.TypeOf(typeof(ZeroPointerFailure))); } /// /// Tests the method with a detached process. - /// It should fail with a . + /// It should fail with a . /// [Test] public void ReadObjectOnDetachedProcessTest() @@ -601,12 +590,12 @@ public void ReadObjectOnDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.Read(typeof(long), OuterClassPointer); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } /// /// Tests the method with a detached process. - /// It should fail with a . + /// It should fail with a . /// [Test] public void ReadObjectWithPointerPathOnDetachedProcessTest() @@ -614,7 +603,7 @@ public void ReadObjectWithPointerPathOnDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.Read(typeof(long), OuterClassPointer.ToString("X")); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } #endregion @@ -662,14 +651,14 @@ public void ReadStructureAsObjectTest() /// /// Tests the method with a structure type that contains reference /// types. - /// It should fail with a . + /// It should fail with a . /// [Test] public void ReadStructureWithReferencesTest() { var result = TestProcessMemory!.Read(OuterClassPointer + 0x38); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnConversionFailure))); + Assert.That(result.Failure, Is.TypeOf(typeof(ConversionFailure))); } #endregion @@ -773,32 +762,32 @@ public void FindStringSettingsOnKnownPathTest() /// /// Tests with a valid address but the wrong /// expected string. - /// The method should return a . + /// The method should return a . /// [Test] public void FindStringSettingsWithWrongStringTest() { var result = TestProcessMemory!.FindStringSettings(GetStringPointerAddress(), "Wrong string"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnNoSettingsFound))); + Assert.That(result.Failure, Is.TypeOf(typeof(UndeterminedStringSettingsFailure))); } /// /// Tests with a valid address but an empty /// expected string. - /// The method should return a . + /// The method should return a . /// [Test] public void FindStringSettingsWithEmptyStringTest() { var result = TestProcessMemory!.FindStringSettings(GetStringPointerAddress(), ""); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnNoSettingsFound))); + Assert.That(result.Failure, Is.TypeOf(typeof(InvalidArgumentFailure))); } /// /// Tests on a zero pointer address. - /// The method should return a . + /// The method should return a . /// [Test] public void FindStringSettingsOnZeroPointerTest() @@ -807,28 +796,26 @@ public void FindStringSettingsOnZeroPointerTest() var allocatedSpace = TestProcessMemory!.Allocate(8, false).Value.ReserveRange(8).Value; var result = TestProcessMemory!.FindStringSettings(allocatedSpace.Address, "Whatever"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnZeroPointer))); + Assert.That(result.Failure, Is.TypeOf(typeof(ZeroPointerFailure))); } /// /// Tests with a pointer at the maximum possible /// address, which is invalid. - /// The method should return a . + /// The method should return an OS failure of some sort. /// [Test] public void FindStringSettingsOnInvalidPointerAddressTest() { var result = TestProcessMemory!.FindStringSettings(GetMaxPointerValue(), "Whatever"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnPointerReadFailure))); - var failure = (FindStringSettingsFailureOnPointerReadFailure)result.Error; - Assert.That(failure.Details, Is.TypeOf(typeof(ReadFailureOnSystemRead))); + Assert.That(result.Failure, Is.TypeOf(typeof(OperatingSystemCallFailure))); } /// /// Tests with a pointer to the maximum possible /// address, which is invalid. - /// The method should return a . + /// The method should return an OS failure of some sort. /// [Test] public void FindStringSettingsOnInvalidStringAddressTest() @@ -837,29 +824,23 @@ public void FindStringSettingsOnInvalidStringAddressTest() PointerPath pointerPath = GetPathToPointerToMaxAddress(); var result = TestProcessMemory!.FindStringSettings(pointerPath, "Whatever"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnStringReadFailure))); - var failure = (FindStringSettingsFailureOnStringReadFailure)result.Error; - Assert.That(failure.Details, Is.TypeOf(typeof(ReadFailureOnSystemRead))); + Assert.That(result.Failure, Is.TypeOf(typeof(OperatingSystemCallFailure))); } /// /// Tests with a pointer path that cannot be - /// evaluated. - /// The method should return a . + /// evaluated. The method should fail. /// [Test] public void FindStringSettingsOnInvalidPointerPathTest() { var result = TestProcessMemory!.FindStringSettings("bad pointer path", "Whatever"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnPointerPathEvaluation))); - var failure = (FindStringSettingsFailureOnPointerPathEvaluation)result.Error; - Assert.That(failure.Details, Is.Not.Null); } /// /// Tests with a detached process. - /// The method should return a . + /// The method should return a . /// [Test] public void FindStringSettingsOnDetachedProcessTest() @@ -867,12 +848,12 @@ public void FindStringSettingsOnDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.FindStringSettings(0x1234, InitialStringValue); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } /// /// Tests with a detached process. - /// The method should return a . + /// The method should return a . /// [Test] public void FindStringSettingsWithPointerPathOnDetachedProcessTest() @@ -880,7 +861,7 @@ public void FindStringSettingsWithPointerPathOnDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.FindStringSettings("1234", InitialStringValue); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } #endregion @@ -950,62 +931,57 @@ public void ReadRawStringWithKnownStringTest() /// /// Tests . /// Call the method with a zero pointer as the address. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadRawStringWithZeroPointerTest() { var result = TestProcessMemory!.ReadRawString(UIntPtr.Zero, Encoding.Unicode); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnZeroPointer))); + Assert.That(result.Failure, Is.TypeOf(typeof(ZeroPointerFailure))); } /// /// Tests . /// Call the method with an unreadable address. - /// Expect the result to be a . + /// Expect the result to be an . /// [Test] public void ReadRawStringWithUnreadableAddressTest() { - var result = TestProcessMemory!.ReadRawString(GetMaxPointerValue(), Encoding.Unicode); + var result = TestProcessMemory!.ReadRawString(1, Encoding.Unicode); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnSystemRead))); - var systemError = ((ReadFailureOnSystemRead)result.Error).Details; - Assert.That(systemError, Is.Not.Null); + Assert.That(result.Failure, Is.TypeOf(typeof(OperatingSystemCallFailure))); } /// /// Tests . /// Call the method with a negative max length. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadRawStringWithNegativeMaxLengthTest() { var result = TestProcessMemory!.ReadRawString(GetPathToRawStringBytes(), Encoding.Unicode, -1); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnInvalidArguments))); + Assert.That(result.Failure, Is.TypeOf(typeof(InvalidArgumentFailure))); } /// /// Tests . /// Call the method with a pointer path that cannot be evaluated. - /// Expect the result to be a . + /// Expect the result to be a failure. /// [Test] public void ReadRawStringWithBadPointerPathTest() { var result = TestProcessMemory!.ReadRawString("bad pointer path", Encoding.Unicode); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnPointerPathEvaluation))); - var pathError = ((ReadFailureOnPointerPathEvaluation)result.Error).Details; - Assert.That(pathError, Is.Not.Null); } /// /// Tests with a detached - /// process. Expect the result to be a . + /// process. Expect the result to be a . /// [Test] public void ReadRawStringWithDetachedProcessTest() @@ -1013,12 +989,12 @@ public void ReadRawStringWithDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.ReadRawString(0x1234, Encoding.Unicode); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } /// /// Tests with a detached - /// process. Expect the result to be a . + /// process. Expect the result to be a . /// [Test] public void ReadRawStringWithPointerPathWithDetachedProcessTest() @@ -1026,7 +1002,7 @@ public void ReadRawStringWithPointerPathWithDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.ReadRawString("1234", Encoding.Unicode); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } #endregion @@ -1095,19 +1071,19 @@ public void ReadStringPointerOnKnownStringTest() /// /// Tests with a zero pointer address. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadStringPointerWithZeroPointerTest() { var result = TestProcessMemory!.ReadStringPointer(UIntPtr.Zero, GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnZeroPointer))); + Assert.That(result.Failure, Is.TypeOf(typeof(ZeroPointerFailure))); } /// /// Tests with a pointer that reads 0. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadStringPointerWithPointerToZeroPointerTest() @@ -1117,41 +1093,36 @@ public void ReadStringPointerWithPointerToZeroPointerTest() var result = TestProcessMemory!.ReadStringPointer(allocatedSpace.Address, GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnZeroPointer))); + Assert.That(result.Failure, Is.TypeOf(typeof(ZeroPointerFailure))); } /// /// Tests with a zero pointer address. - /// Expect the result to be a . + /// Expect the result to be an . /// [Test] public void ReadStringPointerWithUnreadablePointerTest() { var result = TestProcessMemory!.ReadStringPointer(GetMaxPointerValue(), GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnPointerReadFailure))); - var failure = (StringReadFailureOnPointerReadFailure)result.Error; - Assert.That(failure.Details, Is.Not.Null); + Assert.That(result.Failure, Is.TypeOf(typeof(OperatingSystemCallFailure))); } /// /// Tests with an address that points to /// an unreadable memory region. - /// Expect the result to be a . + /// Expect the result to be a failure. /// [Test] public void ReadStringPointerWithPointerToUnreadableMemoryTest() { var result = TestProcessMemory!.ReadStringPointer(GetPathToPointerToMaxAddress(), GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnStringBytesReadFailure))); - var failure = (StringReadFailureOnStringBytesReadFailure)result.Error; - Assert.That(failure.Details, Is.Not.Null); } /// /// Tests with invalid string settings. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadStringPointerWithInvalidSettingsTest() @@ -1161,21 +1132,18 @@ public void ReadStringPointerWithInvalidSettingsTest() lengthPrefix: null); var result = TestProcessMemory!.ReadStringPointer(GetAddressForValueAtIndex(IndexOfOutputString), settings); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnInvalidSettings))); + Assert.That(result.Failure, Is.TypeOf(typeof(InvalidStringSettingsFailure))); } /// /// Tests with a bad pointer path. - /// Expect the result to be a . + /// Expect the result to be a failure. /// [Test] public void ReadStringPointerWithBadPointerPathTest() { var result = TestProcessMemory!.ReadStringPointer("bad pointer path", GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnPointerPathEvaluation))); - var failure = (StringReadFailureOnPointerPathEvaluation)result.Error; - Assert.That(failure.Details, Is.Not.Null); } /// @@ -1184,7 +1152,7 @@ public void ReadStringPointerWithBadPointerPathTest() /// has a value of 10. /// Performs 2 reads: one with a max length of 10 bytes, and one with a max length of 9 bytes. /// Expect the first read to return the full string, and the second read to fail with a - /// . + /// . /// [Test] public void ReadStringPointerWithTooLongStringWithBytesPrefixTest() @@ -1212,8 +1180,8 @@ public void ReadStringPointerWithTooLongStringWithBytesPrefixTest() Assert.That(firstResult.Value, Is.EqualTo(stringContent)); Assert.That(secondResult.IsSuccess, Is.False); - Assert.That(secondResult.Error, Is.TypeOf(typeof(StringReadFailureOnStringTooLong))); - var failure = (StringReadFailureOnStringTooLong)secondResult.Error; + Assert.That(secondResult.Failure, Is.TypeOf(typeof(StringTooLongFailure))); + var failure = (StringTooLongFailure)secondResult.Failure; Assert.That(failure.LengthPrefixValue, Is.EqualTo(10)); } @@ -1223,7 +1191,7 @@ public void ReadStringPointerWithTooLongStringWithBytesPrefixTest() /// that has a value of 10. /// Performs 2 reads: one with a max length of 10 characters, and one with a max length of 9 characters. /// Expect the first read to return the full string, and the second read to fail with a - /// . + /// . /// [Test] public void ReadStringPointerWithTooLongStringWithCharactersPrefixTest() @@ -1252,8 +1220,8 @@ public void ReadStringPointerWithTooLongStringWithCharactersPrefixTest() Assert.That(firstResult.Value, Is.EqualTo(stringContent)); Assert.That(secondResult.IsSuccess, Is.False); - Assert.That(secondResult.Error, Is.TypeOf(typeof(StringReadFailureOnStringTooLong))); - var failure = (StringReadFailureOnStringTooLong)secondResult.Error; + Assert.That(secondResult.Failure, Is.TypeOf(typeof(StringTooLongFailure))); + var failure = (StringTooLongFailure)secondResult.Failure; Assert.That(failure.LengthPrefixValue, Is.EqualTo(10)); } @@ -1262,7 +1230,7 @@ public void ReadStringPointerWithTooLongStringWithCharactersPrefixTest() /// The settings specify no length prefix, but a null terminator. The pointer points to a string with 10 characters. /// Performs 2 reads: one with a max length of 10 characters, and one with a max length of 9 characters. /// Expect the first read to return the full string, and the second read to fail with a - /// . + /// . /// [Test] public void ReadStringPointerWithTooLongStringWithoutPrefixTest() @@ -1290,14 +1258,14 @@ public void ReadStringPointerWithTooLongStringWithoutPrefixTest() Assert.That(firstResult.Value, Is.EqualTo(stringContent)); Assert.That(secondResult.IsSuccess, Is.False); - Assert.That(secondResult.Error, Is.TypeOf(typeof(StringReadFailureOnStringTooLong))); - var failure = (StringReadFailureOnStringTooLong)secondResult.Error; + Assert.That(secondResult.Failure, Is.TypeOf(typeof(StringTooLongFailure))); + var failure = (StringTooLongFailure)secondResult.Failure; Assert.That(failure.LengthPrefixValue, Is.Null); // No prefix, so the value should be null. } /// /// Tests with a detached process. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadStringPointerWithDetachedProcessTest() @@ -1305,12 +1273,12 @@ public void ReadStringPointerWithDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.ReadStringPointer(0x1234, GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } /// /// Tests with a detached process. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadStringPointerWithPointerPathWithDetachedProcessTest() @@ -1318,7 +1286,7 @@ public void ReadStringPointerWithPointerPathWithDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.ReadStringPointer("1234", GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnDetachedProcess))); + Assert.That(result.Failure, Is.TypeOf(typeof(DetachedProcessFailure))); } #endregion @@ -1337,7 +1305,7 @@ public class ProcessMemoryReadTestX86 : ProcessMemoryReadTest /// /// Tests with a 64-bit address on a 32-bit process. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadBytesOnX86WithX64AddressTest() @@ -1345,12 +1313,12 @@ public void ReadBytesOnX86WithX64AddressTest() var address = (ulong)uint.MaxValue + 1; var result = TestProcessMemory!.ReadBytes((UIntPtr)address, 8); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnIncompatibleBitness))); + Assert.That(result.Failure, Is.TypeOf(typeof(IncompatibleBitnessPointerFailure))); } /// /// Tests with a 64-bit address on a 32-bit - /// process. Expect the result to be a . + /// process. Expect the result to be a . /// [Test] public void ReadBytesPartialOnX86WithX64AddressTest() @@ -1358,12 +1326,12 @@ public void ReadBytesPartialOnX86WithX64AddressTest() var address = (ulong)uint.MaxValue + 1; var result = TestProcessMemory!.ReadBytesPartial((UIntPtr)address, new byte[8], 8); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnIncompatibleBitness))); + Assert.That(result.Failure, Is.TypeOf(typeof(IncompatibleBitnessPointerFailure))); } /// /// Tests with a 64-bit address on a 32-bit process. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadGenericOnX86WithX64AddressTest() @@ -1371,12 +1339,12 @@ public void ReadGenericOnX86WithX64AddressTest() var address = (ulong)uint.MaxValue + 1; var result = TestProcessMemory!.Read((UIntPtr)address); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnIncompatibleBitness))); + Assert.That(result.Failure, Is.TypeOf(typeof(IncompatibleBitnessPointerFailure))); } /// /// Tests with a 64-bit address on a 32-bit process. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void ReadObjectOnX86WithX64AddressTest() @@ -1384,12 +1352,12 @@ public void ReadObjectOnX86WithX64AddressTest() var address = (ulong)uint.MaxValue + 1; var result = TestProcessMemory!.Read(typeof(int), (UIntPtr)address); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnIncompatibleBitness))); + Assert.That(result.Failure, Is.TypeOf(typeof(IncompatibleBitnessPointerFailure))); } /// /// Tests with a 64-bit address on a 32-bit process. - /// Expect the result to be a . + /// Expect the result to be a . /// [Test] public void FindStringSettingsOnX86WithX64AddressTest() @@ -1397,12 +1365,12 @@ public void FindStringSettingsOnX86WithX64AddressTest() var address = (ulong)uint.MaxValue + 1; var result = TestProcessMemory!.FindStringSettings((UIntPtr)address, "Whatever"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(FindStringSettingsFailureOnIncompatibleBitness))); + Assert.That(result.Failure, Is.TypeOf(typeof(IncompatibleBitnessPointerFailure))); } /// /// Tests with a 64-bit address on a - /// 32-bit process. Expect the result to be a . + /// 32-bit process. Expect the result to be a . /// [Test] public void ReadRawStringOnX86WithX64AddressTest() @@ -1410,12 +1378,12 @@ public void ReadRawStringOnX86WithX64AddressTest() var address = (ulong)uint.MaxValue + 1; var result = TestProcessMemory!.ReadRawString((UIntPtr)address, Encoding.Unicode); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(ReadFailureOnIncompatibleBitness))); + Assert.That(result.Failure, Is.TypeOf(typeof(IncompatibleBitnessPointerFailure))); } /// /// Tests with a 64-bit address on a 32-bit - /// process. Expect the result to be a . + /// process. Expect the result to be a . /// [Test] public void ReadStringPointerOnX86WithX64AddressTest() @@ -1423,6 +1391,6 @@ public void ReadStringPointerOnX86WithX64AddressTest() var address = (ulong)uint.MaxValue + 1; var result = TestProcessMemory!.ReadStringPointer((UIntPtr)address, GetDotNetStringSettings()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.TypeOf(typeof(StringReadFailureOnIncompatibleBitness))); + Assert.That(result.Failure, Is.TypeOf(typeof(IncompatibleBitnessPointerFailure))); } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryThreadingTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryThreadingTest.cs index 5eda774..b0a60df 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryThreadingTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryThreadingTest.cs @@ -58,9 +58,9 @@ protected virtual Assembler AssembleTrampolineForGetCurrentDirectoryW(UIntPtr fu public void RunThreadAndWaitTest() { var threadResult = TestProcessMemory!.RunThread("kernel32.dll", "ExitProcess"); - Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Error.ToString()); + Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Failure.ToString()); var waitResult = threadResult.Value.WaitForCompletion(TimeSpan.FromSeconds(10)); - Assert.That(waitResult.IsSuccess, Is.True, () => waitResult.Error.ToString()); + Assert.That(waitResult.IsSuccess, Is.True, () => waitResult.Failure.ToString()); Assert.That(waitResult.Value, Is.Zero); // The exit code should be 0 Assert.That(HasProcessExited, Is.True); // The process should have exited as a result of the function call } @@ -75,9 +75,9 @@ public void RunThreadWithPointerPathTest() var kernel32Module = TestProcessMemory!.GetModule("kernel32.dll"); var functionAddress = kernel32Module!.ReadExportTable().Value["ExitProcess"]; var threadResult = TestProcessMemory!.RunThread(functionAddress.ToString("X")); - Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Error.ToString()); + Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Failure.ToString()); var waitResult = threadResult.Value.WaitForCompletion(TimeSpan.FromSeconds(10)); - Assert.That(waitResult.IsSuccess, Is.True, () => waitResult.Error.ToString()); + Assert.That(waitResult.IsSuccess, Is.True, () => waitResult.Failure.ToString()); Assert.That(waitResult.Value, Is.Zero); Assert.That(HasProcessExited, Is.True); } @@ -103,13 +103,14 @@ public void RunThreadWithGetCurrentDirectoryWTrampolineTest() // We cannot call GetCurrentDirectoryW directly because of its parameters. We need to write a trampoline // function that prepares the parameters as they are expected by the function before calling it. var assembler = AssembleTrampolineForGetCurrentDirectoryW(functionAddress); - var codeReservation = TestProcessMemory.StoreCode(assembler, kernel32Module.GetRange().Start).Value; + var codeReservation = TestProcessMemory + .StoreCode(assembler, nearAddress: kernel32Module.GetRange().Start).Value; // Run the thread var threadResult = TestProcessMemory!.RunThread(codeReservation.Address, argsReservation.Address); - Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Error.ToString()); + Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Failure.ToString()); var waitResult = threadResult.Value.WaitForCompletion(TimeSpan.FromSeconds(10)); - Assert.That(waitResult.IsSuccess, Is.True, () => waitResult.Error.ToString()); + Assert.That(waitResult.IsSuccess, Is.True, () => waitResult.Failure.ToString()); // Read the resulting string from the allocated buffer var resultingString = TestProcessMemory.ReadRawString(bufferReservation.Address, Encoding.Unicode, 512).Value; @@ -120,17 +121,17 @@ public void RunThreadWithGetCurrentDirectoryWTrampolineTest() /// Tests . /// Runs the Sleep kernel32.dll function in a thread in the target process. /// Waits for the resulting thread, but with a timeout that does not leave enough time for the function to complete. - /// Check that the wait operation returns a error. + /// Check that the wait operation returns a error. /// [Test] public void RunThreadSleepTimeoutTest() { // Start a Sleep thread that will run for 5 seconds, but wait only for 500ms var threadResult = TestProcessMemory!.RunThread("kernel32.dll", "Sleep", 5000); - Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Error.ToString()); + Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Failure.ToString()); var waitResult = threadResult.Value.WaitForCompletion(TimeSpan.FromMilliseconds(500)); Assert.That(waitResult.IsSuccess, Is.False); - Assert.That(waitResult.Error, Is.TypeOf()); + Assert.That(waitResult.Failure, Is.TypeOf()); } /// @@ -143,12 +144,12 @@ public void RunThreadSleepTimeoutTest() [Test] public async Task RunThreadSleepWaitAsyncTest() { - var tasks = new List>>(); + var tasks = new List>>(); for (int i = 0; i < 10; i++) { // Each thread executes Sleep for 500ms var threadResult = TestProcessMemory!.RunThread("kernel32.dll", "Sleep", 500); - Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Error.ToString()); + Assert.That(threadResult.IsSuccess, Is.True, () => threadResult.Failure.ToString()); tasks.Add(threadResult.Value.WaitForCompletionAsync(TimeSpan.FromSeconds(10))); } @@ -158,7 +159,7 @@ public async Task RunThreadSleepWaitAsyncTest() foreach (var task in tasks) { - Assert.That(task.Result.IsSuccess, Is.True, () => task.Result.Error.ToString()); + Assert.That(task.Result.IsSuccess, Is.True, () => task.Result.Failure.ToString()); Assert.That(task.Result.Value, Is.Zero); } @@ -171,57 +172,56 @@ public async Task RunThreadSleepWaitAsyncTest() /// /// Tests with an invalid /// pointer path. - /// Expects a error. + /// Expects a failure. /// [Test] public void RunThreadWithInvalidPointerPathTest() { var threadResult = TestProcessMemory!.RunThread("invalid pointer path"); Assert.That(threadResult.IsSuccess, Is.False); - Assert.That(threadResult.Error, Is.TypeOf()); } /// /// Tests with a zero address. - /// Expects a error. + /// Expects a error. /// [Test] public void RunThreadWithZeroPointerTest() { var threadResult = TestProcessMemory!.RunThread(UIntPtr.Zero); Assert.That(threadResult.IsSuccess, Is.False); - Assert.That(threadResult.Error, Is.TypeOf()); + Assert.That(threadResult.Failure, Is.TypeOf()); } /// /// Tests with a module name /// that does not match any module loaded in the process. - /// Expects a error. + /// Expects a error. /// [Test] public void RunThreadWithInvalidModuleTest() { var threadResult = TestProcessMemory!.RunThread("invalid module", "ExitProcess"); Assert.That(threadResult.IsSuccess, Is.False); - Assert.That(threadResult.Error, Is.TypeOf()); + Assert.That(threadResult.Failure, Is.TypeOf()); } /// /// Tests with a valid module /// name but a function name that does not match any exported function in the module. - /// Expects a error. + /// Expects a error. /// [Test] public void RunThreadWithInvalidFunctionTest() { var threadResult = TestProcessMemory!.RunThread("kernel32.dll", "invalid function"); Assert.That(threadResult.IsSuccess, Is.False); - Assert.That(threadResult.Error, Is.TypeOf()); + Assert.That(threadResult.Failure, Is.TypeOf()); } /// /// Tests with a detached process. - /// Expects a error. + /// Expects a error. /// [Test] public void RunThreadWithAddressOnDetachedProcessTest() @@ -229,12 +229,12 @@ public void RunThreadWithAddressOnDetachedProcessTest() TestProcessMemory!.Dispose(); var threadResult = TestProcessMemory!.RunThread(0x1234); Assert.That(threadResult.IsSuccess, Is.False); - Assert.That(threadResult.Error, Is.TypeOf()); + Assert.That(threadResult.Failure, Is.TypeOf()); } /// /// Tests with a detached - /// process. Expects a error. + /// process. Expects a error. /// [Test] public void RunThreadWithPointerPathOnDetachedProcessTest() @@ -242,12 +242,12 @@ public void RunThreadWithPointerPathOnDetachedProcessTest() TestProcessMemory!.Dispose(); var threadResult = TestProcessMemory!.RunThread("1234"); Assert.That(threadResult.IsSuccess, Is.False); - Assert.That(threadResult.Error, Is.TypeOf()); + Assert.That(threadResult.Failure, Is.TypeOf()); } /// /// Tests with a detached - /// process. Expects a error. + /// process. Expects a error. /// [Test] public void RunThreadWithExportedFunctionOnDetachedProcessTest() @@ -255,7 +255,7 @@ public void RunThreadWithExportedFunctionOnDetachedProcessTest() TestProcessMemory!.Dispose(); var threadResult = TestProcessMemory!.RunThread("kernel32.dll", "Sleep", 2000); Assert.That(threadResult.IsSuccess, Is.False); - Assert.That(threadResult.Error, Is.TypeOf()); + Assert.That(threadResult.Failure, Is.TypeOf()); } } @@ -293,7 +293,7 @@ protected override Assembler AssembleTrampolineForGetCurrentDirectoryW(UIntPtr f /// /// Tests with an address that is /// beyond the scope of a 32-bit process. - /// Expect a error. + /// Expect a error. /// [Test] public void RunThreadWithIncompatibleAddressTest() @@ -301,13 +301,13 @@ public void RunThreadWithIncompatibleAddressTest() UIntPtr maxAddress = uint.MaxValue; var threadResult = TestProcessMemory!.RunThread(maxAddress + 1); Assert.That(threadResult.IsSuccess, Is.False); - Assert.That(threadResult.Error, Is.TypeOf()); + Assert.That(threadResult.Failure, Is.TypeOf()); } /// /// Tests with a parameter /// value that is beyond the scope of a 32-bit process. - /// Expect a error. + /// Expect a error. /// [Test] public void RunThreadWithIncompatibleParameterTest() @@ -315,6 +315,6 @@ public void RunThreadWithIncompatibleParameterTest() UIntPtr maxValue = uint.MaxValue; var threadResult = TestProcessMemory!.RunThread("kernel32.dll", "ExitProcess", maxValue + 1); Assert.That(threadResult.IsSuccess, Is.False); - Assert.That(threadResult.Error, Is.TypeOf()); + Assert.That(threadResult.Failure, Is.TypeOf()); } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs index 37fd387..a37c214 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs @@ -85,46 +85,48 @@ public void WriteStringPointerTest() /// /// Tests with a zero pointer. - /// Expect a error. + /// Expect a error. /// [Test] public void WriteAtZeroPointerTest() { var result = TestProcessMemory!.Write(UIntPtr.Zero, 8); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } /// /// Tests with a null value. - /// Expect a error. + /// Expect a error. /// [Test] public void WriteNullValueTest() { var result = TestProcessMemory!.Write(OuterClassPointer, (int?)null); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } /// /// Tests with an unsupported - /// type. Expect a error. + /// type. Expect a error. /// [Test] public void WriteUnsupportedTypeTest() { var result = TestProcessMemory!.Write(OuterClassPointer, new object()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } + #pragma warning disable 0649 /// Defines a structure that is expected to be incompatible with writing methods. private struct IncompatibleStruct { public long A; public byte[] B; } // The byte[] makes it incompatible + #pragma warning restore 0649 /// /// Tests with an incompatible - /// struct. Expect a error. + /// struct. Expect a error. /// /// /// This test has been disabled because it triggers a System.AccessViolationException. This exception type used to @@ -135,12 +137,12 @@ public void WriteIncompatibleStructTest() { var result = TestProcessMemory!.Write(OuterClassPointer, new IncompatibleStruct()); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } /// /// Tests with a detached - /// process. Expect a error. + /// process. Expect a error. /// [Test] public void WriteWithDetachedProcessTest() @@ -148,12 +150,12 @@ public void WriteWithDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.Write(OuterClassPointer, 8); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } /// /// Tests with a detached - /// process. Expect a error. + /// process. Expect a error. /// [Test] public void WriteAtPointerPathWithDetachedProcessTest() @@ -161,7 +163,7 @@ public void WriteAtPointerPathWithDetachedProcessTest() TestProcessMemory!.Dispose(); var result = TestProcessMemory.Write(OuterClassPointer.ToString("X"), 8); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } } @@ -176,7 +178,7 @@ public class ProcessMemoryWriteTestX86 : ProcessMemoryWriteTest /// /// Tests on a 32-bit target app - /// with a 64-bit address. Expect a error. + /// with a 64-bit address. Expect a error. /// [Test] public void WriteGenericOnX86WithX64AddressTest() @@ -184,12 +186,12 @@ public void WriteGenericOnX86WithX64AddressTest() var address = (ulong)uint.MaxValue + 1; var result = TestProcessMemory!.Write((UIntPtr)address, 8); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } /// /// Tests on a 32-bit - /// target app with a 64-bit address. Expect a error. + /// target app with a 64-bit address. Expect a error. /// [Test] public void WriteBytesOnX86WithX64AddressTest() @@ -197,13 +199,13 @@ public void WriteBytesOnX86WithX64AddressTest() var address = (ulong)uint.MaxValue + 1; var result = TestProcessMemory!.WriteBytes((UIntPtr)address, new byte[8]); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } /// /// Tests on a 32-bit target app /// with a reachable address, but a value with a pointer type that goes beyond the 32-bit address space. - /// Expect a error. + /// Expect a error. /// [Test] public void WriteX64PointerOnX86Test() @@ -211,6 +213,6 @@ public void WriteX64PointerOnX86Test() var address = (ulong)uint.MaxValue + 1; var result = TestProcessMemory!.Write(OuterClassPointer, (UIntPtr)address); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.Error, Is.InstanceOf()); + Assert.That(result.Failure, Is.InstanceOf()); } } \ No newline at end of file diff --git a/test/MindControl.Test/ProcessMemoryTests/RemoteModuleTest.cs b/test/MindControl.Test/ProcessMemoryTests/RemoteModuleTest.cs index 33e0005..ee6a5e5 100644 --- a/test/MindControl.Test/ProcessMemoryTests/RemoteModuleTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/RemoteModuleTest.cs @@ -41,7 +41,7 @@ public void ReadExportTableWithKernel32Test() { var module = TestProcessMemory!.GetModule("kernel32.dll") ?? throw new Exception("Module not found"); var exportTable = module.ReadExportTable(); - Assert.That(exportTable.IsSuccess, Is.True, () => exportTable.Error); + Assert.That(exportTable.IsSuccess, Is.True, () => exportTable.Failure.ToString()); Assert.That(exportTable.Value, Has.Count.GreaterThan(1000)); Assert.That(exportTable.Value.ContainsKey("LoadLibraryW")); Assert.That(exportTable.Value.Values.Select(t => module.GetRange().Contains(t)), Is.All.True); diff --git a/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs b/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs index f1fc084..cdde4b3 100644 --- a/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs +++ b/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs @@ -61,7 +61,7 @@ public void TryParseTest(PatternTestCase testCase) if (testCase.Expected == null) { Assert.That(result.IsFailure, Is.True); - Assert.That(result.Error.Message, Is.Not.Empty); + Assert.That(result.Failure.Message, Is.Not.Empty); } else { From b87fd15d8498b2f3c1c013ebb5c547b650f24f41 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 31 May 2025 16:39:21 +0200 Subject: [PATCH 48/66] First commit of a long series on publishing the docs --- .github/workflows/publish.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 105c008..05622df 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -110,4 +110,25 @@ jobs: run: | foreach($file in (Get-ChildItem "${{ env.NuGetDirectory }}" -Recurse -Include *.nupkg)) { dotnet nuget push $file --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate - } \ No newline at end of file + } + publish_docs: + if: github.event_name == 'release' + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + + - name: Install DocFX + run: dotnet tool install -g docfx + + - name: Build documentation + run: docfx docs/docfx.json + + - name: Publish to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/_site + publish_branch: gh-pages \ No newline at end of file From 090066ef6cb4bd0983bb9c498633734b9121c95b Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 31 May 2025 16:46:29 +0200 Subject: [PATCH 49/66] Adjustments to the workflow --- .github/workflows/publish.yml | 4 +++- src/MindControl.Code/MindControl.Code.csproj | 2 +- src/MindControl/MindControl.csproj | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 05622df..5291fc2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -77,8 +77,10 @@ jobs: - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 + - name: Restore NuGet packages + run: dotnet restore - # Build the InjectedLibrary C++ project that is required by the test project. + # Build the InjectedLibrary C++ project required by the test project. - name: Setup MSBuild uses: microsoft/setup-msbuild@v2 - name: Build the C++ injection test DLL diff --git a/src/MindControl.Code/MindControl.Code.csproj b/src/MindControl.Code/MindControl.Code.csproj index 6e24b45..6cc1645 100644 --- a/src/MindControl.Code/MindControl.Code.csproj +++ b/src/MindControl.Code/MindControl.Code.csproj @@ -8,7 +8,7 @@ MindControl.Code https://github.com/Doublevil/mind-control git - 1.0.0 + 1.0.0-alpha-25053101 diff --git a/src/MindControl/MindControl.csproj b/src/MindControl/MindControl.csproj index 6c91d69..d39e715 100644 --- a/src/MindControl/MindControl.csproj +++ b/src/MindControl/MindControl.csproj @@ -7,7 +7,7 @@ MindControl https://github.com/Doublevil/mind-control git - 1.0.0-alpha-25032201 + 1.0.0-alpha-25053101 From 9c4406b5d0fb929fecb40b8340a90c97be02a28c Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 31 May 2025 17:03:44 +0200 Subject: [PATCH 50/66] Adjusted workflow and csproj for the test project --- .github/workflows/publish.yml | 18 +++++++++++++----- test/MindControl.Test/MindControl.Test.csproj | 3 ++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5291fc2..ea8a5ce 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -79,13 +79,21 @@ jobs: uses: actions/setup-dotnet@v4 - name: Restore NuGet packages run: dotnet restore - - # Build the InjectedLibrary C++ project required by the test project. + + # Build the InjectedLibrary C++ project in both x64 and x86 configurations - name: Setup MSBuild uses: microsoft/setup-msbuild@v2 - - name: Build the C++ injection test DLL - run: msbuild test/MindControl.Test.InjectedLibrary/MindControl.Test.InjectedLibrary.vcxproj /p:Configuration=Release /p:Platform=x64 /p:OutDir=../MindControl.Test/bin/Release/ - + - name: Build the C++ injection test DLL x64 + run: msbuild test/MindControl.Test.InjectedLibrary/MindControl.Test.InjectedLibrary.vcxproj /p:Configuration=Release /p:Platform=x64 /p:OutDir=../MindControl.Test/bin/Release/InjectedLibrary/x64/ + - name: Build the C++ injection test DLL x86 + run: msbuild test/MindControl.Test.InjectedLibrary/MindControl.Test.InjectedLibrary.vcxproj /p:Configuration=Release /p:Platform=x86 /p:OutDir=../MindControl.Test/bin/Release/InjectedLibrary/x86/ + + # Build the test target project in x64 and x86 + - name: Build the test target project x64 + run: dotnet build test/MindControl.Test/MindControl.Test.csproj --configuration Release --runtime win-x64 --output ../MindControl.Test/bin/Release/TargetApp/x64/ + - name: Build the test target project x86 + run: dotnet build test/MindControl.Test/MindControl.Test.csproj --configuration Release --runtime win-x86 --output ../MindControl.Test/bin/Release/TargetApp/x86/ + # Run the tests - name: Run tests run: dotnet test test/MindControl.Test/MindControl.Test.csproj --configuration Release diff --git a/test/MindControl.Test/MindControl.Test.csproj b/test/MindControl.Test/MindControl.Test.csproj index cb850ff..fb2b178 100644 --- a/test/MindControl.Test/MindControl.Test.csproj +++ b/test/MindControl.Test/MindControl.Test.csproj @@ -29,7 +29,8 @@ - + Date: Sat, 31 May 2025 17:09:25 +0200 Subject: [PATCH 51/66] Fixed test target project path in workflow --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ea8a5ce..1e9ad6b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -90,9 +90,9 @@ jobs: # Build the test target project in x64 and x86 - name: Build the test target project x64 - run: dotnet build test/MindControl.Test/MindControl.Test.csproj --configuration Release --runtime win-x64 --output ../MindControl.Test/bin/Release/TargetApp/x64/ + run: dotnet build test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x64 --output ../MindControl.Test/bin/Release/TargetApp/x64/ - name: Build the test target project x86 - run: dotnet build test/MindControl.Test/MindControl.Test.csproj --configuration Release --runtime win-x86 --output ../MindControl.Test/bin/Release/TargetApp/x86/ + run: dotnet build test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x86 --output ../MindControl.Test/bin/Release/TargetApp/x86/ # Run the tests - name: Run tests From 9313d23df56339503e8a3807987c8102a516ec79 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 31 May 2025 17:21:38 +0200 Subject: [PATCH 52/66] Replaced build with publish for the target app in workflow --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1e9ad6b..a72fd5f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -90,9 +90,9 @@ jobs: # Build the test target project in x64 and x86 - name: Build the test target project x64 - run: dotnet build test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x64 --output ../MindControl.Test/bin/Release/TargetApp/x64/ + run: dotnet publish test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x64 --output ../MindControl.Test/bin/Release/TargetApp/x64/ - name: Build the test target project x86 - run: dotnet build test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x86 --output ../MindControl.Test/bin/Release/TargetApp/x86/ + run: dotnet publish test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x86 --output ../MindControl.Test/bin/Release/TargetApp/x86/ # Run the tests - name: Run tests From 20802aa16071d3c88d407126fc8943794c1951e5 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 31 May 2025 17:32:29 +0200 Subject: [PATCH 53/66] Added debug step in workflow to help understand errors --- .github/workflows/publish.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a72fd5f..0e96c47 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -94,6 +94,9 @@ jobs: - name: Build the test target project x86 run: dotnet publish test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x86 --output ../MindControl.Test/bin/Release/TargetApp/x86/ + - name: DEBUG List published files + run: Get-ChildItem -Recurse test/MindControl.Test/bin/Release + # Run the tests - name: Run tests run: dotnet test test/MindControl.Test/MindControl.Test.csproj --configuration Release From dbb4c2515048879564e59850dc2a64d1bdec18dc Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 31 May 2025 17:46:27 +0200 Subject: [PATCH 54/66] More debugging on the TargetApp workflow mystery --- .github/workflows/publish.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0e96c47..ed01ff6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -90,12 +90,15 @@ jobs: # Build the test target project in x64 and x86 - name: Build the test target project x64 - run: dotnet publish test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x64 --output ../MindControl.Test/bin/Release/TargetApp/x64/ + run: dotnet publish test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x64 --output ${{ github.workspace }}/test/MindControl.Test/bin/Release/TargetApp/x64/ - name: Build the test target project x86 - run: dotnet publish test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x86 --output ../MindControl.Test/bin/Release/TargetApp/x86/ + run: dotnet publish test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x86 --output ${{ github.workspace }}/test/MindControl.Test/bin/Release/TargetApp/x86/ - name: DEBUG List published files - run: Get-ChildItem -Recurse test/MindControl.Test/bin/Release + run: Get-ChildItem -Recurse test/MindControl.Test/bin + + - name: DEBUG List published files + run: Get-ChildItem -Recurse test/MindControl.Test.TargetApp/bin # Run the tests - name: Run tests From 1cb24ca963c19bb4fb992dacafdd0d1012c316ca Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 31 May 2025 18:04:45 +0200 Subject: [PATCH 55/66] Removed debug listings from workflow. Now debugging tests --- .github/workflows/publish.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ed01ff6..52a2294 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -94,15 +94,9 @@ jobs: - name: Build the test target project x86 run: dotnet publish test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x86 --output ${{ github.workspace }}/test/MindControl.Test/bin/Release/TargetApp/x86/ - - name: DEBUG List published files - run: Get-ChildItem -Recurse test/MindControl.Test/bin - - - name: DEBUG List published files - run: Get-ChildItem -Recurse test/MindControl.Test.TargetApp/bin - # Run the tests - name: Run tests - run: dotnet test test/MindControl.Test/MindControl.Test.csproj --configuration Release + run: dotnet test test/MindControl.Test/MindControl.Test.csproj --configuration Release --logger "console;verbosity=detailed" deploy: # Publish only when creating a GitHub Release From d4de0cf33b6ffb96d8c4cd888bb8ec287266d937 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 31 May 2025 18:35:58 +0200 Subject: [PATCH 56/66] Trying to address hostfxr.dll issues with self contained target builds --- .github/workflows/publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 52a2294..000094f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -90,9 +90,9 @@ jobs: # Build the test target project in x64 and x86 - name: Build the test target project x64 - run: dotnet publish test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x64 --output ${{ github.workspace }}/test/MindControl.Test/bin/Release/TargetApp/x64/ + run: dotnet publish test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x64 --self-contained true --output ${{ github.workspace }}/test/MindControl.Test/bin/Release/TargetApp/x64/ - name: Build the test target project x86 - run: dotnet publish test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x86 --output ${{ github.workspace }}/test/MindControl.Test/bin/Release/TargetApp/x86/ + run: dotnet publish test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj --configuration Release --runtime win-x86 --self-contained true --output ${{ github.workspace }}/test/MindControl.Test/bin/Release/TargetApp/x86/ # Run the tests - name: Run tests From 3e8aa226d1ffa6b59511bac4ef880614ac9ce8bf Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sun, 1 Jun 2025 00:51:06 +0200 Subject: [PATCH 57/66] Added failure reasons for failed results in tests --- .../MindControl.Test.TargetApp.csproj | 4 +- .../ProcessMemoryStreamTest.cs | 4 +- .../AllocationTests/MemoryAllocationTest.cs | 4 +- test/MindControl.Test/MindControl.Test.csproj | 4 +- .../ProcessMemoryCodeExtensionsTest.cs | 6 +-- .../ProcessMemoryHookExtensionsTest.cs | 36 ++++++++-------- .../ProcessMemoryAllocationTest.cs | 42 +++++++++---------- .../ProcessMemoryAnchorTest.cs | 16 +++---- .../ProcessMemoryEvaluateTest.cs | 4 +- .../ProcessMemoryInjectionTest.cs | 2 +- .../ProcessMemoryReadTest.cs | 30 ++++++------- .../ProcessMemoryWriteTest.cs | 2 +- .../SearchTests/ByteSearchPatternTest.cs | 2 +- 13 files changed, 78 insertions(+), 78 deletions(-) diff --git a/test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj b/test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj index 40018b0..350819b 100644 --- a/test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj +++ b/test/MindControl.Test.TargetApp/MindControl.Test.TargetApp.csproj @@ -9,8 +9,8 @@ True - - x86 + + win-x64;win-x86 diff --git a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs index 386eb3a..e23df69 100644 --- a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs +++ b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs @@ -277,7 +277,7 @@ public void ReadOnTheEdgeOfValidMemoryTest() var allocatedMemory = TestProcessMemory!.Allocate(0x1000, false).Value; var targetAddress = allocatedMemory.Range.End - 4; var writeResult = TestProcessMemory.WriteBytes(targetAddress, bytesAtTheEnd, MemoryProtectionStrategy.Ignore); - Assert.That(writeResult.IsSuccess, Is.True); + Assert.That(writeResult.IsSuccess, Is.True, writeResult.ToString()); // Attempt to read 8 bytes from the target address, which is 4 bytes before the end of the isolated segment. using var stream = TestProcessMemory.GetMemoryStream(targetAddress); @@ -416,7 +416,7 @@ public void WriteOnTheEdgeOfValidMemoryTest() var allocatedMemory = TestProcessMemory!.Allocate(0x1000, false).Value; var targetAddress = allocatedMemory.Range.End - 4; var writeResult = TestProcessMemory.WriteBytes(targetAddress, bytesAtTheEnd, MemoryProtectionStrategy.Ignore); - Assert.That(writeResult.IsSuccess, Is.True); + Assert.That(writeResult.IsSuccess, Is.True, writeResult.ToString()); // Attempt to write 8 bytes from the target address, which is 4 bytes before the end of the isolated segment. var stream = TestProcessMemory.GetMemoryStream(targetAddress); diff --git a/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs b/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs index 19b1b1c..75e91a3 100644 --- a/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs +++ b/test/MindControl.Test/AllocationTests/MemoryAllocationTest.cs @@ -56,7 +56,7 @@ public void DisposeTest() public void ReserveRangeTest() { var result = _allocation.ReserveRange(0x10); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); var reservedRange = result.Value; @@ -78,7 +78,7 @@ public void ReserveRangeTest() public void ReserveRangeWithFullRangeTest() { var result = _allocation.ReserveRange(_allocation.Range.GetSize()); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); } /// diff --git a/test/MindControl.Test/MindControl.Test.csproj b/test/MindControl.Test/MindControl.Test.csproj index fb2b178..7cc6136 100644 --- a/test/MindControl.Test/MindControl.Test.csproj +++ b/test/MindControl.Test/MindControl.Test.csproj @@ -33,10 +33,10 @@ Condition="'$(AUTOMATED_BUILD)' != 'true'"> diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs index 2a9ac58..95ddecc 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryCodeExtensionsTest.cs @@ -23,7 +23,7 @@ public void DisableCodeAtTest() { var movIntAddress = FindMovIntAddress(); var result = TestProcessMemory!.DisableCodeAt(movIntAddress); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); Assert.That(result.Value.Address, Is.EqualTo(movIntAddress)); Assert.That(result.Value.Length, Is.AtLeast(1)); // We don't care how long it is but we check that it is set. @@ -43,7 +43,7 @@ public void DisableCodeAtWithPointerPathTest() { var pointerPath = FindMovIntAddress().ToString("X"); var result = TestProcessMemory!.DisableCodeAt(pointerPath); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); ProceedUntilProcessEnds(); AssertFinalResults(IndexOfOutputInt, InitialIntValue.ToString(CultureInfo.InvariantCulture)); @@ -60,7 +60,7 @@ public void DisableCodeAtWithPointerPathTest() public void DisableCodeAtWithMultipleInstructionsTest() { var result = TestProcessMemory!.DisableCodeAt(FindMovIntAddress(), 3); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); ProceedUntilProcessEnds(); diff --git a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs index fb6055f..5336460 100644 --- a/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/CodeExtensions/ProcessMemoryHookExtensionsTest.cs @@ -50,7 +50,7 @@ public void HookAndReplaceMovWithByteArrayTest() var hookResult = TestProcessMemory!.Hook(movIntAddress, bytes, new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); Assert.That(hookResult.Value.InjectedCodeReservation, Is.Not.Null); Assert.That(hookResult.Value.Address, Is.EqualTo(movIntAddress)); Assert.That(hookResult.Value.Length, Is.AtLeast(5)); @@ -77,7 +77,7 @@ public void HookAndInsertMovWithoutRegisterIsolationTest() var hookResult = TestProcessMemory!.Hook(FindMovIntAddress(), bytes, new HookOptions(HookExecutionMode.ExecuteInjectedCodeFirst)); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); ProceedUntilProcessEnds(); @@ -98,7 +98,7 @@ public void HookAndInsertMovWithRegisterIsolationWithByteArrayTest() var hookResult = TestProcessMemory!.Hook(FindMovIntAddress(), bytes, new HookOptions(HookExecutionMode.ExecuteInjectedCodeFirst, HookRegister.RcxEcx)); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); ProceedUntilProcessEnds(); AssertExpectedFinalResults(); @@ -116,7 +116,7 @@ public void HookAndReplaceMovWithAssemblerTest() var hookResult = TestProcessMemory!.Hook(targetInstructionAddress, assembler, new HookOptions(HookExecutionMode.ReplaceOriginalInstruction)); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); Assert.That(hookResult.Value.InjectedCodeReservation, Is.Not.Null); Assert.That(hookResult.Value.Address, Is.EqualTo(targetInstructionAddress)); Assert.That(hookResult.Value.Length, Is.AtLeast(5)); @@ -137,7 +137,7 @@ public void HookAndInsertMovWithRegisterIsolationWithAssemblerTest() var hookResult = TestProcessMemory!.Hook(FindMovIntAddress(), assembler, new HookOptions(HookExecutionMode.ExecuteInjectedCodeFirst, HookRegister.RcxEcx)); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); ProceedUntilProcessEnds(); AssertExpectedFinalResults(); @@ -307,7 +307,7 @@ public void InsertCodeAtWithByteArrayTest() var targetInstructionAddress = FindMovIntAddress(); var hookResult = TestProcessMemory!.InsertCodeAt(targetInstructionAddress, bytes); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); Assert.That(hookResult.Value.InjectedCodeReservation, Is.Not.Null); Assert.That(hookResult.Value.Address, Is.EqualTo(targetInstructionAddress)); Assert.That(hookResult.Value.Length, Is.AtLeast(5)); @@ -328,7 +328,7 @@ public void InsertCodeAtWithByteArrayWithIsolationTest() var bytes = AssembleRcxMov(0).AssembleToBytes().Value; var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress(), bytes, HookRegister.RcxEcx); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); ProceedUntilProcessEnds(); AssertExpectedFinalResults(); @@ -345,7 +345,7 @@ public void InsertCodeAtWithAssemblerTest() var assembler = AssembleRcxMov(reservation.Address.ToUInt32()); var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress(), assembler); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); ProceedUntilProcessEnds(); AssertFinalResults(IndexOfOutputInt, InitialIntValue.ToString()); @@ -359,7 +359,7 @@ public void InsertCodeAtWithAssemblerTest() public void InsertCodeAtWithAssemblerWithIsolationTest() { var hookResult = TestProcessMemory!.InsertCodeAt(FindMovIntAddress(), AssembleRcxMov(0), HookRegister.RcxEcx); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); ProceedUntilProcessEnds(); AssertExpectedFinalResults(); } @@ -376,7 +376,7 @@ public void InsertCodeAtWithByteArrayWithPointerPathTest() PointerPath targetInstructionPath = FindMovIntAddress().ToString("X"); var hookResult = TestProcessMemory!.InsertCodeAt(targetInstructionPath, bytes); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); ProceedUntilProcessEnds(); AssertFinalResults(IndexOfOutputInt, InitialIntValue.ToString()); @@ -394,7 +394,7 @@ public void InsertCodeAtWithAssemblerWithPointerPathTest() var targetInstructionPath = FindMovIntAddress().ToString("X"); var hookResult = TestProcessMemory!.InsertCodeAt(targetInstructionPath, assembler); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); ProceedUntilProcessEnds(); AssertFinalResults(IndexOfOutputInt, InitialIntValue.ToString()); @@ -523,7 +523,7 @@ public void ReplaceCodeAtWithByteArrayTest() var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; var replaceResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 1, bytes); - Assert.That(replaceResult.IsSuccess, Is.True); + Assert.That(replaceResult.IsSuccess, Is.True, replaceResult.ToString()); // Check that the result is a CodeChange and not a hook, because the new instructions should fit. Assert.That(replaceResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); @@ -544,7 +544,7 @@ public void ReplaceCodeAtWithByteArrayOnMultipleInstructionsTest() var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; var replaceResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 3, bytes); - Assert.That(replaceResult.IsSuccess, Is.True); + Assert.That(replaceResult.IsSuccess, Is.True, replaceResult.ToString()); Assert.That(replaceResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); ProceedUntilProcessEnds(); @@ -569,7 +569,7 @@ public void ReplaceCodeAtWithByteArrayWithPreservedRegistersTest() { var bytes = AssembleAlternativeMovInt().AssembleToBytes().Value; var hookResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 1, bytes, HookRegister.RaxEax); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); Assert.That(hookResult.Value, Is.TypeOf()); ProceedUntilProcessEnds(); @@ -587,7 +587,7 @@ public void ReplaceCodeAtWithByteArrayWithPointerPathTest() PointerPath targetPath = FindMovIntAddress().ToString("X"); var replaceResult = TestProcessMemory!.ReplaceCodeAt(targetPath, 1, bytes); - Assert.That(replaceResult.IsSuccess, Is.True); + Assert.That(replaceResult.IsSuccess, Is.True, replaceResult.ToString()); Assert.That(replaceResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); ProceedUntilProcessEnds(); @@ -604,7 +604,7 @@ public void ReplaceCodeAtWithAssemblerTest() var assembler = AssembleAlternativeMovInt(); var replaceResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 1, assembler); - Assert.That(replaceResult.IsSuccess, Is.True); + Assert.That(replaceResult.IsSuccess, Is.True, replaceResult.ToString()); Assert.That(replaceResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); ProceedUntilProcessEnds(); @@ -621,7 +621,7 @@ public void ReplaceCodeAtWithAssemblerWithPreservedRegistersTest() { var assembler = AssembleAlternativeMovInt(); var hookResult = TestProcessMemory!.ReplaceCodeAt(FindMovIntAddress(), 1, assembler, HookRegister.RaxEax); - Assert.That(hookResult.IsSuccess, Is.True); + Assert.That(hookResult.IsSuccess, Is.True, hookResult.ToString()); Assert.That(hookResult.Value, Is.TypeOf()); ProceedUntilProcessEnds(); @@ -639,7 +639,7 @@ public void ReplaceCodeAtWithAssemblerWithPointerPathTest() PointerPath targetPath = FindMovIntAddress().ToString("X"); var replaceResult = TestProcessMemory!.ReplaceCodeAt(targetPath, 1, assembler); - Assert.That(replaceResult.IsSuccess, Is.True); + Assert.That(replaceResult.IsSuccess, Is.True, replaceResult.ToString()); Assert.That(replaceResult.Value.GetType(), Is.EqualTo(typeof(CodeChange))); ProceedUntilProcessEnds(); diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index e8c657c..299d37f 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -20,7 +20,7 @@ public void AllocateTest() { var allocationResult = TestProcessMemory!.Allocate(0x1000, false); - Assert.That(allocationResult.IsSuccess, Is.True); + Assert.That(allocationResult.IsSuccess, Is.True, allocationResult.ToString()); var allocation = allocationResult.Value; // To check that the memory is writable, we will write a byte array to the allocated range. @@ -44,7 +44,7 @@ public void AllocateExecutableTest() { var allocationResult = TestProcessMemory!.Allocate(0x1000, true); - Assert.That(allocationResult.IsSuccess, Is.True); + Assert.That(allocationResult.IsSuccess, Is.True, allocationResult.ToString()); var allocation = allocationResult.Value; Assert.That(allocation, Is.Not.Null); Assert.That(allocation.IsDisposed, Is.False); @@ -65,8 +65,8 @@ public void AllocateWithinRangeTest() var range = new MemoryRange(new UIntPtr(0x120000), UIntPtr.MaxValue); var allocationWithoutRangeResult = TestProcessMemory!.Allocate(0x1000, false); var allocationWithRangeResult = TestProcessMemory!.Allocate(0x1000, false, range); - Assert.That(allocationWithoutRangeResult.IsSuccess, Is.True); - Assert.That(allocationWithRangeResult.IsSuccess, Is.True); + Assert.That(allocationWithoutRangeResult.IsSuccess, Is.True, allocationWithoutRangeResult.ToString()); + Assert.That(allocationWithRangeResult.IsSuccess, Is.True, allocationWithRangeResult.ToString()); Assert.That(range.Contains(allocationWithoutRangeResult.Value.Range), Is.False); Assert.That(range.Contains(allocationWithRangeResult.Value.Range), Is.True); } @@ -82,8 +82,8 @@ public void AllocateNearAddressTest() var nearAddress = new UIntPtr(0x400000000000); var allocationWithNearAddressResult = TestProcessMemory!.Allocate(0x1000, false, nearAddress: nearAddress); var allocationWithoutNearAddressResult = TestProcessMemory!.Allocate(0x1000, false); - Assert.That(allocationWithoutNearAddressResult.IsSuccess, Is.True); - Assert.That(allocationWithNearAddressResult.IsSuccess, Is.True); + Assert.That(allocationWithoutNearAddressResult.IsSuccess, Is.True, allocationWithoutNearAddressResult.ToString()); + Assert.That(allocationWithNearAddressResult.IsSuccess, Is.True, allocationWithNearAddressResult.ToString()); Assert.That(allocationWithNearAddressResult.Value.Range.DistanceTo(nearAddress), Is.LessThan(allocationWithoutNearAddressResult.Value.Range.DistanceTo(nearAddress))); @@ -124,7 +124,7 @@ public void ReserveWithAvailableAllocationTest() { var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; var reservationResult = TestProcessMemory.Reserve(0x1000, false); - Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); var reservation = reservationResult.Value; Assert.That(reservation.Range.GetSize(), Is.EqualTo(0x1000)); Assert.That(reservation.ParentAllocation, Is.EqualTo(allocation)); @@ -140,7 +140,7 @@ public void ReserveWithAvailableAllocationTest() public void ReserveWithoutAvailableAllocationTest() { var reservationResult = TestProcessMemory!.Reserve(0x1000, false); - Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); Assert.That(reservationResult.Value.Range.GetSize(), Is.EqualTo(0x1000)); } @@ -157,7 +157,7 @@ public void ReserveForDataWithAvailableCodeAllocationTest() { var allocation = TestProcessMemory!.Allocate(0x1000, true).Value; var reservationResult = TestProcessMemory.Reserve(0x1000, false); - Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); var reservation = reservationResult.Value; Assert.That(reservation.ParentAllocation, Is.EqualTo(allocation)); Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); @@ -175,7 +175,7 @@ public void ReserveForCodeWithAvailableDataAllocationTest() { var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; var reservationResult = TestProcessMemory.Reserve(0x1000, true); - Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); var reservation = reservationResult.Value; Assert.That(reservation.ParentAllocation, Is.Not.EqualTo(allocation)); Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(2)); @@ -193,7 +193,7 @@ public void ReserveForCodeWithAvailableCodeAllocationTest() { var allocation = TestProcessMemory!.Allocate(0x1000, true).Value; var reservationResult = TestProcessMemory.Reserve(0x1000, true); - Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); var reservation = reservationResult.Value; Assert.That(reservation.ParentAllocation, Is.EqualTo(allocation)); Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(1)); @@ -209,7 +209,7 @@ public void ReserveTooLargeForAvailableAllocationsTest() { var allocation = TestProcessMemory!.Allocate(0x1000, true).Value; var reservationResult = TestProcessMemory.Reserve(0x2000, true); - Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); var reservation = reservationResult.Value; Assert.That(reservation.ParentAllocation, Is.Not.EqualTo(allocation)); Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(2)); @@ -227,7 +227,7 @@ public void ReserveWithLimitRangeTest() var range = new MemoryRange(unchecked((UIntPtr)0x400000000000), UIntPtr.MaxValue); var allocation = TestProcessMemory!.Allocate(0x1000, true).Value; var reservationResult = TestProcessMemory.Reserve(0x1000, true, range); - Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); var reservation = reservationResult.Value; Assert.That(reservation.ParentAllocation, Is.Not.EqualTo(allocation)); Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(2)); @@ -251,7 +251,7 @@ public void ReserveWithNearAddressTest() new MemoryRange(unchecked((UIntPtr)0x4B0000000000), UIntPtr.MaxValue)); var reservationResult = TestProcessMemory.Reserve(0x1000, true, nearAddress:unchecked((UIntPtr)0x2000051C0000)); - Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); var reservation = reservationResult.Value; Assert.That(reservation.ParentAllocation, Is.EqualTo(allocation2)); Assert.That(TestProcessMemory.Allocations, Has.Count.EqualTo(3)); @@ -293,7 +293,7 @@ public void StoreWithAllocationTest() var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; var reservationResult = TestProcessMemory.Store(value, allocation); - Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); var reservation = reservationResult.Value; byte[] read = TestProcessMemory.ReadBytes(reservation.Range.Start, value.Length).Value; @@ -315,7 +315,7 @@ public void StoreWithoutPreAllocationTest() var value = new byte[] { 1, 2, 3, 4 }; var reservationResult = TestProcessMemory!.Store(value); - Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); var reservation = reservationResult.Value; var read = TestProcessMemory.ReadBytes(reservation.Range.Start, value.Length).Value; @@ -338,7 +338,7 @@ public void StoreWithoutPreAllocationWithMultipleSmallValuesTest() var reservationResults = Enumerable.Range(0, 4).Select(_ => TestProcessMemory!.Store(value)).ToList(); foreach (var result in reservationResults) - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); var reservations = reservationResults.Select(r => r.Value).ToList(); var readBackValues = reservations.Select(r => TestProcessMemory!.ReadBytes(r.Range.Start, value.Length) @@ -370,14 +370,14 @@ public void StoreWithMultipleOverflowingValuesTest() var firstStoreResult = TestProcessMemory!.Store(value); // So far, we should have only one allocated range. - Assert.That(firstStoreResult.IsSuccess, Is.True); + Assert.That(firstStoreResult.IsSuccess, Is.True, firstStoreResult.ToString()); Assert.That(TestProcessMemory!.Allocations, Has.Count.EqualTo(1)); // Now we store the same value again, which should overflow the range. var secondStoreResult = TestProcessMemory!.Store(value); // We should have two allocated ranges now, because there is no room left in the first range. - Assert.That(secondStoreResult.IsSuccess, Is.True); + Assert.That(secondStoreResult.IsSuccess, Is.True, secondStoreResult.ToString()); Assert.That(TestProcessMemory!.Allocations, Has.Count.EqualTo(2)); } @@ -418,7 +418,7 @@ public void StoreStringWithoutAllocationTest() { var stringToStore = "Hello 世界!"; var reservationResult = TestProcessMemory!.StoreString(stringToStore, GetDotNetStringSettings()); - Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); var reservation = reservationResult.Value; var bytesReadBack = TestProcessMemory.ReadBytes(reservation.Address, reservation.Range.GetSize()).Value; @@ -444,7 +444,7 @@ public void StoreStringWithAllocationTest() var allocation = TestProcessMemory!.Allocate(0x1000, false).Value; var reservationResult = TestProcessMemory.StoreString(stringToStore, GetDotNetStringSettings(), allocation); - Assert.That(reservationResult.IsSuccess, Is.True); + Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); var reservation = reservationResult.Value; var bytesReadBack = TestProcessMemory.ReadBytes(reservation.Address, reservation.Range.GetSize()).Value; diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs index 2c3c761..114102b 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAnchorTest.cs @@ -20,7 +20,7 @@ public void ReadIntAtStaticAddressTest() { var anchor = TestProcessMemory!.GetAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); var readResult = anchor.Read(); - Assert.That(readResult.IsSuccess, Is.True); + Assert.That(readResult.IsSuccess, Is.True, readResult.ToString()); Assert.That(readResult.Value, Is.EqualTo(InitialIntValue)); } @@ -33,7 +33,7 @@ public void ReadIntAtPointerPathTest() { var anchor = TestProcessMemory!.GetAnchor(GetPointerPathForValueAtIndex(IndexOfOutputInt)); var readResult = anchor.Read(); - Assert.That(readResult.IsSuccess, Is.True); + Assert.That(readResult.IsSuccess, Is.True, readResult.ToString()); Assert.That(readResult.Value, Is.EqualTo(InitialIntValue)); } @@ -48,7 +48,7 @@ public void WriteIntTest() int newValue = 1234567; var anchor = TestProcessMemory!.GetAnchor(GetAddressForValueAtIndex(IndexOfOutputInt)); var writeResult = anchor.Write(newValue); - Assert.That(writeResult.IsSuccess, Is.True); + Assert.That(writeResult.IsSuccess, Is.True, writeResult.ToString()); ProceedToNextStep(); AssertFinalResults(IndexOfOutputInt, newValue.ToString(CultureInfo.InvariantCulture)); } @@ -173,7 +173,7 @@ public void ReadBytesAtStaticAddressTest() GetPointerPathForValueAtIndex(IndexOfOutputByteArray)).Value; var anchor = TestProcessMemory.GetByteArrayAnchor(address, InitialByteArrayValue.Length); var readResult = anchor.Read(); - Assert.That(readResult.IsSuccess, Is.True); + Assert.That(readResult.IsSuccess, Is.True, readResult.ToString()); Assert.That(readResult.Value, Is.EqualTo(InitialByteArrayValue)); } @@ -187,7 +187,7 @@ public void ReadBytesAtPointerPathTest() var anchor = TestProcessMemory!.GetByteArrayAnchor( GetPointerPathForValueAtIndex(IndexOfOutputByteArray), InitialByteArrayValue.Length); var readResult = anchor.Read(); - Assert.That(readResult.IsSuccess, Is.True); + Assert.That(readResult.IsSuccess, Is.True, readResult.ToString()); Assert.That(readResult.Value, Is.EqualTo(InitialByteArrayValue)); } @@ -204,7 +204,7 @@ public void WriteBytesTest() var anchor = TestProcessMemory!.GetByteArrayAnchor( GetPointerPathForValueAtIndex(IndexOfOutputByteArray), InitialByteArrayValue.Length); var writeResult = anchor.Write(newValue); - Assert.That(writeResult.IsSuccess, Is.True); + Assert.That(writeResult.IsSuccess, Is.True, writeResult.ToString()); ProceedToNextStep(); AssertFinalResults(IndexOfOutputByteArray, "14,24,34,44"); } @@ -223,7 +223,7 @@ public void ReadStringPointerAtStaticAddressTest() var anchor = TestProcessMemory!.GetStringPointerAnchor( GetAddressForValueAtIndex(IndexOfOutputString), GetDotNetStringSettings()); var readResult = anchor.Read(); - Assert.That(readResult.IsSuccess, Is.True); + Assert.That(readResult.IsSuccess, Is.True, readResult.ToString()); Assert.That(readResult.Value, Is.EqualTo(InitialStringValue)); } @@ -237,7 +237,7 @@ public void ReadStringPointerAtPointerPathTest() var anchor = TestProcessMemory!.GetStringPointerAnchor( GetPointerPathForValueAtIndex(IndexOfOutputString), GetDotNetStringSettings()); var readResult = anchor.Read(); - Assert.That(readResult.IsSuccess, Is.True); + Assert.That(readResult.IsSuccess, Is.True, readResult.ToString()); Assert.That(readResult.Value, Is.EqualTo(InitialStringValue)); } diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs index 30f3554..cf9eabd 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryEvaluateTest.cs @@ -19,7 +19,7 @@ public void EvaluateOnKnownPointerTest() { var result = TestProcessMemory!.EvaluateMemoryAddress(GetPathToMaxAddress()); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); Assert.That(result.Value, Is.EqualTo(GetMaxPointerValue())); } @@ -31,7 +31,7 @@ public void EvaluateWithModuleTest() { var result = TestProcessMemory!.EvaluateMemoryAddress($"{MainModuleName}+8"); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); Assert.That(result.Value, Is.Not.EqualTo(UIntPtr.Zero)); } diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs index da16465..b8774a8 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryInjectionTest.cs @@ -62,7 +62,7 @@ public void InjectLibraryWithNonAsciiPathTest() File.Copy(GetInjectedLibraryPath(), targetPath, true); var result = TestProcessMemory!.InjectLibrary(targetPath); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); var output = ProceedToNextStep(); Assert.That(output, Is.EqualTo("Injected library attached")); } diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index 80fb7a0..b17d192 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -75,7 +75,7 @@ public void ReadBytesWithInvalidPathTest() public void ReadBytesWithZeroLengthTest() { var result = TestProcessMemory!.ReadBytes(OuterClassPointer, 0); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); Assert.That(result.Value, Is.Empty); } @@ -210,7 +210,7 @@ public void ReadBytesPartialWithInvalidPathTest() public void ReadBytesPartialWithZeroLengthTest() { var result = TestProcessMemory!.ReadBytesPartial(OuterClassPointer, [], 0); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); Assert.That(result.Value, Is.Zero); } @@ -241,13 +241,13 @@ public void ReadBytesPartialOnPartiallyUnreadableRangeTest() var allocatedMemory = TestProcessMemory!.Allocate(0x1000, false).Value; var targetAddress = allocatedMemory.Range.End - 4; var writeResult = TestProcessMemory.WriteBytes(targetAddress, bytesAtTheEnd, MemoryProtectionStrategy.Ignore); - Assert.That(writeResult.IsSuccess, Is.True); + Assert.That(writeResult.IsSuccess, Is.True, writeResult.ToString()); // Read 8 bytes, which should result in reading 4 bytes from the readable region and 4 bytes from the unreadable // one. var buffer = new byte[8]; var result = TestProcessMemory.ReadBytesPartial(targetAddress, buffer, 8); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); Assert.That(result.Value, Is.EqualTo(4)); Assert.That(buffer, Is.EqualTo(new byte[] { 0x1, 0x2, 0x3, 0x4, 0, 0, 0, 0 })); } @@ -725,7 +725,7 @@ public void FindStringSettingsTest(StringSettingsTestCase testCase) // Call the tested method on the string pointer (the one we wrote last) var findSettingsResult = TestProcessMemory.FindStringSettings(allocatedSpace.Address, testCase.String); - Assert.That(findSettingsResult.IsSuccess, Is.True); + Assert.That(findSettingsResult.IsSuccess, Is.True, findSettingsResult.ToString()); // Check that the settings match the test case, i.e. that the determined settings are the same settings we // used to write the string in memory. @@ -749,7 +749,7 @@ public void FindStringSettingsOnKnownPathTest() var dotNetStringSettings = GetDotNetStringSettings(); var result = TestProcessMemory!.FindStringSettings(GetPointerPathForValueAtIndex(IndexOfOutputString), InitialStringValue); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); Assert.That(result.Value.Encoding, Is.EqualTo(dotNetStringSettings.Encoding)); Assert.That(result.Value.IsNullTerminated, Is.EqualTo(dotNetStringSettings.IsNullTerminated)); Assert.That(result.Value.LengthPrefix?.Size, @@ -900,7 +900,7 @@ public void ReadRawStringTest(ReadRawStringTestCase testCase) var result = TestProcessMemory.ReadRawString(reservedMemory.Address, testCase.Encoding, testCase.MaxLength, testCase.IsNullTerminated); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); var resultString = result.Value; var expectedString = testCase.ExpectedStringIfDifferent ?? testCase.String; @@ -921,10 +921,10 @@ public void ReadRawStringWithKnownStringTest() ProceedToNextStep(); var secondResult = TestProcessMemory.ReadRawString(path, Encoding.Unicode); - Assert.That(firstResult.IsSuccess, Is.True); + Assert.That(firstResult.IsSuccess, Is.True, firstResult.ToString()); Assert.That(firstResult.Value, Is.EqualTo(InitialStringValue)); - Assert.That(secondResult.IsSuccess, Is.True); + Assert.That(secondResult.IsSuccess, Is.True, secondResult.ToString()); Assert.That(secondResult.Value, Is.EqualTo(ExpectedFinalStringValue)); } @@ -1044,7 +1044,7 @@ public void ReadStringPointerTest(StringSettingsTestCase testCase) // Call the tested method on the string pointer (the one we wrote last) and check that we got the same string // that we wrote previously. var result = TestProcessMemory.ReadStringPointer(allocatedSpace.Address, settings); - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); Assert.That(result.Value, Is.EqualTo(testCase.String)); } @@ -1062,10 +1062,10 @@ public void ReadStringPointerOnKnownStringTest() ProceedToNextStep(); var secondResult = TestProcessMemory.ReadStringPointer(path, GetDotNetStringSettings()); - Assert.That(firstResult.IsSuccess, Is.True); + Assert.That(firstResult.IsSuccess, Is.True, firstResult.ToString()); Assert.That(firstResult.Value, Is.EqualTo(InitialStringValue)); - Assert.That(secondResult.IsSuccess, Is.True); + Assert.That(secondResult.IsSuccess, Is.True, secondResult.ToString()); Assert.That(secondResult.Value, Is.EqualTo(ExpectedFinalStringValue)); } @@ -1176,7 +1176,7 @@ public void ReadStringPointerWithTooLongStringWithBytesPrefixTest() settings.MaxLength = 9; var secondResult = TestProcessMemory.ReadStringPointer(allocatedSpace.Address, settings); - Assert.That(firstResult.IsSuccess, Is.True); + Assert.That(firstResult.IsSuccess, Is.True, firstResult.ToString()); Assert.That(firstResult.Value, Is.EqualTo(stringContent)); Assert.That(secondResult.IsSuccess, Is.False); @@ -1216,7 +1216,7 @@ public void ReadStringPointerWithTooLongStringWithCharactersPrefixTest() settings.MaxLength = 9; var secondResult = TestProcessMemory.ReadStringPointer(allocatedSpace.Address, settings); - Assert.That(firstResult.IsSuccess, Is.True); + Assert.That(firstResult.IsSuccess, Is.True, firstResult.ToString()); Assert.That(firstResult.Value, Is.EqualTo(stringContent)); Assert.That(secondResult.IsSuccess, Is.False); @@ -1254,7 +1254,7 @@ public void ReadStringPointerWithTooLongStringWithoutPrefixTest() settings.MaxLength = 9; var secondResult = TestProcessMemory.ReadStringPointer(allocatedSpace.Address, settings); - Assert.That(firstResult.IsSuccess, Is.True); + Assert.That(firstResult.IsSuccess, Is.True, firstResult.ToString()); Assert.That(firstResult.Value, Is.EqualTo(stringContent)); Assert.That(secondResult.IsSuccess, Is.False); diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs index a37c214..5354d1c 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryWriteTest.cs @@ -54,7 +54,7 @@ public void WriteStructTest() var readBackResult = TestProcessMemory.Read(pointerPath); - Assert.That(readBackResult.IsSuccess, Is.True); + Assert.That(readBackResult.IsSuccess, Is.True, readBackResult.ToString()); Assert.That(readBackResult.Value, Is.EqualTo(structInstance)); } diff --git a/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs b/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs index cdde4b3..4d6dc06 100644 --- a/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs +++ b/test/MindControl.Test/SearchTests/ByteSearchPatternTest.cs @@ -65,7 +65,7 @@ public void TryParseTest(PatternTestCase testCase) } else { - Assert.That(result.IsSuccess, Is.True); + Assert.That(result.IsSuccess, Is.True, result.ToString()); var pattern = result.Value; Assert.That(pattern.ToString(), Is.EqualTo(testCase.Pattern)); Assert.That(pattern.ByteArray, Is.EqualTo(testCase.Expected.Value.ByteArray)); From f6f69d46cb6b4eaac731ced8c870775e531ea7ac Mon Sep 17 00:00:00 2001 From: Doublevil Date: Thu, 5 Jun 2025 20:46:14 +0200 Subject: [PATCH 58/66] Fixes to allocations and partial reading --- .../Native/IOperatingSystemService.cs | 5 +- src/MindControl/Native/MemoryRangeMetadata.cs | 7 +++ src/MindControl/Native/Win32Service.cs | 11 +++-- .../ProcessMemory/ProcessMemory.Addressing.cs | 2 +- .../ProcessMemory/ProcessMemory.Allocation.cs | 47 +++++++++++-------- src/MindControl/Results/ReadFailure.cs | 2 +- .../AddressingTests/MemoryRangeTest.cs | 24 +++++----- .../ProcessMemoryStreamTest.cs | 8 ++-- .../ProcessMemoryAllocationTest.cs | 21 +++++---- .../ProcessMemoryReadTest.cs | 10 ++-- 10 files changed, 81 insertions(+), 56 deletions(-) diff --git a/src/MindControl/Native/IOperatingSystemService.cs b/src/MindControl/Native/IOperatingSystemService.cs index 2b81089..6fd792a 100644 --- a/src/MindControl/Native/IOperatingSystemService.cs +++ b/src/MindControl/Native/IOperatingSystemService.cs @@ -132,9 +132,10 @@ Result CreateRemoteThread(IntPtr processHandle, UIntPtr startAddress, Result CloseHandle(IntPtr handle); /// - /// Gets the range of memory addressable by applications in the current system. + /// Gets the range of memory addressable by applications of the given bitness in the current system. /// - MemoryRange GetFullMemoryRange(); + /// A boolean indicating if the target application is 64-bit or not. + MemoryRange GetFullMemoryRange(bool is64Bit); /// /// Gets the metadata of a memory region in the virtual address space of a process. diff --git a/src/MindControl/Native/MemoryRangeMetadata.cs b/src/MindControl/Native/MemoryRangeMetadata.cs index 3f2b587..7283aea 100644 --- a/src/MindControl/Native/MemoryRangeMetadata.cs +++ b/src/MindControl/Native/MemoryRangeMetadata.cs @@ -49,4 +49,11 @@ public struct MemoryRangeMetadata /// Gets a boolean indicating if the memory is mapped to a file. /// public bool IsMapped { get; init; } + + /// Returns the fully qualified type name of this instance. + /// The fully qualified type name. + public override string ToString() + { + return $"[{StartAddress:X}-{StartAddress + Size - 1:X}] ({(IsCommitted ? "C" : "-")}{(IsFree ? "F" : "-")}{(IsProtected ? "P" : "-")}{(IsReadable ? "R" : "-")}{(IsWritable ? "W" : "-")}{(IsExecutable ? "E" : "-")}{(IsMapped ? "M" : "-")})"; + } } \ No newline at end of file diff --git a/src/MindControl/Native/Win32Service.cs b/src/MindControl/Native/Win32Service.cs index 65d8e45..c84d89b 100644 --- a/src/MindControl/Native/Win32Service.cs +++ b/src/MindControl/Native/Win32Service.cs @@ -168,7 +168,7 @@ public Result ReadProcessMemoryPartial(IntPtr processHandle, UIntPtr base return initialReadError; // If we found a readable address within the range, we read up to that point. - var newLength = lastReadableAddress.Value.ToUInt64() - baseAddress.ToUInt64(); + var newLength = lastReadableAddress.Value.ToUInt64() - baseAddress.ToUInt64() + 1; returnValue = ReadProcessMemory(processHandle, baseAddress, bufferPtr, newLength, out bytesRead); // If the function failed again and didn't read any byte again, return the read error. @@ -197,7 +197,7 @@ public Result ReadProcessMemoryPartial(IntPtr processHandle, UIntPtr base private UIntPtr? GetLastConsecutiveReadableAddressWithinRange(IntPtr processHandle, bool is64Bit, MemoryRange range) { - var applicationMemoryLimit = GetFullMemoryRange().End; + var applicationMemoryLimit = GetFullMemoryRange(is64Bit).End; ulong rangeEnd = Math.Min(range.End.ToUInt64(), applicationMemoryLimit.ToUInt64()); UIntPtr currentAddress = range.Start; while (currentAddress.ToUInt64() <= rangeEnd) @@ -426,10 +426,13 @@ private SystemInfo GetSystemInfo() /// /// Gets the range of memory addressable by applications in the current system. /// - public MemoryRange GetFullMemoryRange() + /// A boolean indicating if the target application is 64-bit or not. + public MemoryRange GetFullMemoryRange(bool is64Bit) { var systemInfo = GetSystemInfo(); - return new MemoryRange(systemInfo.MinimumApplicationAddress, systemInfo.MaximumApplicationAddress); + var maxAddress = is64Bit ? systemInfo.MaximumApplicationAddress + : Math.Min(uint.MaxValue, systemInfo.MaximumApplicationAddress); + return new MemoryRange(systemInfo.MinimumApplicationAddress, maxAddress); } /// diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs index c04c4e7..2007a80 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Addressing.cs @@ -146,7 +146,7 @@ public ProcessMemoryStream GetMemoryStream(UIntPtr startAddress) /// The clamped memory range, or the full memory range if the input is null. private MemoryRange GetClampedMemoryRange(MemoryRange? input) { - var fullMemoryRange = _osService.GetFullMemoryRange(); + var fullMemoryRange = _osService.GetFullMemoryRange(Is64Bit); return input == null ? fullMemoryRange : new MemoryRange( Start: input.Value.Start.ToUInt64() < fullMemoryRange.Start.ToUInt64() ? fullMemoryRange.Start : input.Value.Start, diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index 417d8fc..8fd4e16 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -73,7 +73,7 @@ internal void Free(MemoryAllocation allocation) private Result FindAndAllocateFreeMemory(ulong sizeNeeded, bool forExecutableCode, MemoryRange? limitRange = null, UIntPtr? nearAddress = null) { - var maxRange = _osService.GetFullMemoryRange(); + var maxRange = _osService.GetFullMemoryRange(Is64Bit); var actualRange = limitRange == null ? maxRange : maxRange.Intersect(limitRange.Value); // If the given range is not within the process applicative memory, return null @@ -91,39 +91,44 @@ private Result FindAndAllocateFreeMemory(ulong sizeNeeded, // For near address search, we are going to search back and forth around the address, which complicates the // process a bit. It means we have to keep track of both the next lowest address and next highest address. var nextAddress = nearAddress ?? actualRange.Value.Start; - var nextAddressForward = nextAddress; - var nextAddressBackwards = nextAddress; + var nextRegionStartAddress = nextAddress; + var previousRegionEndAddress = nextAddress; + var highestAddressAttempted = nextAddress; + var lowestAddressAttempted = nextAddress; bool goingForward = true; MemoryRange? freeRange = null; MemoryRangeMetadata currentMetadata; - while ((nextAddress.ToUInt64() <= actualRange.Value.End.ToUInt64() - && nextAddress.ToUInt64() >= actualRange.Value.Start.ToUInt64()) + while (nextAddress.ToUInt64() <= actualRange.Value.End.ToUInt64() + && nextAddress.ToUInt64() >= actualRange.Value.Start.ToUInt64() && (currentMetadata = _osService.GetRegionMetadata(ProcessHandle, nextAddress) .ValueOrDefault()).Size.ToUInt64() > 0) { - nextAddressForward = (UIntPtr)Math.Max(nextAddressForward.ToUInt64(), + nextRegionStartAddress = (UIntPtr)Math.Max(nextRegionStartAddress.ToUInt64(), nextAddress.ToUInt64() + currentMetadata.Size.ToUInt64()); - nextAddressBackwards = (UIntPtr)Math.Min(nextAddressBackwards.ToUInt64(), + previousRegionEndAddress = (UIntPtr)Math.Min(previousRegionEndAddress.ToUInt64(), currentMetadata.StartAddress.ToUInt64() - 1); + highestAddressAttempted = Math.Max(highestAddressAttempted, nextAddress); + lowestAddressAttempted = Math.Min(lowestAddressAttempted, nextAddress); - nextAddress = goingForward ? nextAddressForward : nextAddressBackwards; + var currentAddress = nextAddress; // If the current region cannot be used, reinitialize the current free range and keep iterating if (!currentMetadata.IsFree) { freeRange = null; - + // In a near address search, we may change direction there depending on which next address is closest. if (nearAddress != null) { - var forwardDistance = nearAddress.Value.DistanceTo(nextAddressForward); - var backwardDistance = nearAddress.Value.DistanceTo(nextAddressBackwards); + var forwardDistance = nearAddress.Value.DistanceTo(nextRegionStartAddress); + var backwardDistance = nearAddress.Value.DistanceTo(previousRegionEndAddress); goingForward = forwardDistance <= backwardDistance - && nextAddressForward.ToUInt64() <= actualRange.Value.End.ToUInt64(); - nextAddress = goingForward ? nextAddressForward : nextAddressBackwards; + && nextRegionStartAddress.ToUInt64() <= actualRange.Value.End.ToUInt64(); } + // Travel to the next region + nextAddress = goingForward ? nextRegionStartAddress : previousRegionEndAddress; continue; } @@ -131,14 +136,14 @@ private Result FindAndAllocateFreeMemory(ulong sizeNeeded, // Extend the free range if it's not null, so that we can have ranges that span across multiple regions. if (goingForward) { - freeRange = new MemoryRange(freeRange?.Start ?? currentMetadata.StartAddress, - (UIntPtr)(currentMetadata.StartAddress.ToUInt64() + currentMetadata.Size.ToUInt64())); + freeRange = new MemoryRange(freeRange?.Start ?? currentAddress, + (UIntPtr)(currentMetadata.StartAddress.ToUInt64() + currentMetadata.Size.ToUInt64() - 1)); } else { freeRange = new MemoryRange(currentMetadata.StartAddress, (UIntPtr)(freeRange?.End.ToUInt64() ?? currentMetadata.StartAddress.ToUInt64() - + currentMetadata.Size.ToUInt64())); + + currentMetadata.Size.ToUInt64() - 1)); } if (freeRange.Value.GetSize() >= sizeNeeded) @@ -163,15 +168,19 @@ private Result FindAndAllocateFreeMemory(ulong sizeNeeded, freeRange = null; // In a near address search, we may change direction there depending on which next address is closest. + var nextAddressForward = highestAddressAttempted + pageSize; + var nextAddressBackward = lowestAddressAttempted - pageSize; if (nearAddress != null) { var forwardDistance = nearAddress.Value.DistanceTo(nextAddressForward); - var backwardDistance = nearAddress.Value.DistanceTo(nextAddressBackwards); + var backwardDistance = nearAddress.Value.DistanceTo(nextAddressBackward); goingForward = forwardDistance <= backwardDistance - && nextAddressForward.ToUInt64() <= actualRange.Value.End.ToUInt64(); - nextAddress = goingForward ? nextAddressForward : nextAddressBackwards; + && nextRegionStartAddress.ToUInt64() <= actualRange.Value.End.ToUInt64(); } + // Travel one page size forward or backward + nextAddress = goingForward ? nextAddressForward : nextAddressBackward; + continue; } } diff --git a/src/MindControl/Results/ReadFailure.cs b/src/MindControl/Results/ReadFailure.cs index b8be573..93e00fd 100644 --- a/src/MindControl/Results/ReadFailure.cs +++ b/src/MindControl/Results/ReadFailure.cs @@ -6,7 +6,7 @@ /// /// Address that caused the failure. public record IncompatibleBitnessPointerFailure(UIntPtr Address) - : Failure($"The address to read, {Address}, is a 64-bit address, but the target process is 32-bit.") + : Failure($"The address to read, {Address:X}, is a 64-bit address, but the target process is 32-bit.") { /// Address that caused the failure. public UIntPtr Address { get; init; } = Address; diff --git a/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs b/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs index eb58d6c..6c09573 100644 --- a/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs +++ b/test/MindControl.Test/AddressingTests/MemoryRangeTest.cs @@ -269,21 +269,21 @@ public record struct AlignedToTestCase(ulong Start, ulong End, uint Alignment, RangeAlignmentMode AlignmentMode, MemoryRange? ExpectedResult); private static AlignedToTestCase[] _alignedToTestCases = { - new(0x1000, 0x1FFF, 4, RangeAlignmentMode.AlignBlock, new MemoryRange((UIntPtr)0x1000, (UIntPtr)0x1FFF)), - new(0x1000, 0x1FFF, 4, RangeAlignmentMode.AlignStart, new MemoryRange((UIntPtr)0x1000, (UIntPtr)0x1FFF)), - new(0x1000, 0x1FFF, 4, RangeAlignmentMode.None, new MemoryRange((UIntPtr)0x1000, (UIntPtr)0x1FFF)), + new(0x1000, 0x1FFF, 4, RangeAlignmentMode.AlignBlock, new MemoryRange(0x1000, 0x1FFF)), + new(0x1000, 0x1FFF, 4, RangeAlignmentMode.AlignStart, new MemoryRange(0x1000, 0x1FFF)), + new(0x1000, 0x1FFF, 4, RangeAlignmentMode.None, new MemoryRange(0x1000, 0x1FFF)), - new(0x1000, 0x1FFF, 8, RangeAlignmentMode.AlignBlock, new MemoryRange((UIntPtr)0x1000, (UIntPtr)0x1FFF)), - new(0x1000, 0x1FFF, 8, RangeAlignmentMode.AlignStart, new MemoryRange((UIntPtr)0x1000, (UIntPtr)0x1FFF)), - new(0x1000, 0x1FFF, 8, RangeAlignmentMode.None, new MemoryRange((UIntPtr)0x1000, (UIntPtr)0x1FFF)), + new(0x1000, 0x1FFF, 8, RangeAlignmentMode.AlignBlock, new MemoryRange(0x1000, 0x1FFF)), + new(0x1000, 0x1FFF, 8, RangeAlignmentMode.AlignStart, new MemoryRange(0x1000, 0x1FFF)), + new(0x1000, 0x1FFF, 8, RangeAlignmentMode.None, new MemoryRange(0x1000, 0x1FFF)), - new(0x1001, 0x1FFE, 4, RangeAlignmentMode.AlignBlock, new MemoryRange((UIntPtr)0x1004, (UIntPtr)0x1FFB)), - new(0x1001, 0x1FFE, 4, RangeAlignmentMode.AlignStart, new MemoryRange((UIntPtr)0x1004, (UIntPtr)0x1FFE)), - new(0x1001, 0x1FFE, 4, RangeAlignmentMode.None, new MemoryRange((UIntPtr)0x1001, (UIntPtr)0x1FFE)), + new(0x1001, 0x1FFE, 4, RangeAlignmentMode.AlignBlock, new MemoryRange(0x1004, 0x1FFB)), + new(0x1001, 0x1FFE, 4, RangeAlignmentMode.AlignStart, new MemoryRange(0x1004, 0x1FFE)), + new(0x1001, 0x1FFE, 4, RangeAlignmentMode.None, new MemoryRange(0x1001, 0x1FFE)), - new(0x1001, 0x1FFE, 8, RangeAlignmentMode.AlignBlock, new MemoryRange((UIntPtr)0x1008, (UIntPtr)0x1FF7)), - new(0x1001, 0x1FFE, 8, RangeAlignmentMode.AlignStart, new MemoryRange((UIntPtr)0x1008, (UIntPtr)0x1FFE)), - new(0x1001, 0x1FFE, 8, RangeAlignmentMode.None, new MemoryRange((UIntPtr)0x1001, (UIntPtr)0x1FFE)), + new(0x1001, 0x1FFE, 8, RangeAlignmentMode.AlignBlock, new MemoryRange(0x1008, 0x1FF7)), + new(0x1001, 0x1FFE, 8, RangeAlignmentMode.AlignStart, new MemoryRange(0x1008, 0x1FFE)), + new(0x1001, 0x1FFE, 8, RangeAlignmentMode.None, new MemoryRange(0x1001, 0x1FFE)), new(0x1000, 0x1FFF, 0x2000, RangeAlignmentMode.AlignBlock, null) }; diff --git a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs index e23df69..976e9fd 100644 --- a/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs +++ b/test/MindControl.Test/AddressingTests/ProcessMemoryStreamTest.cs @@ -273,7 +273,7 @@ public void ReadOnTheEdgeOfValidMemoryTest() { // Prepare a segment of memory that is isolated from other memory regions, and has a known sequence of bytes // at the end. - var bytesAtTheEnd = new byte[] { 0x1, 0x2, 0x3, 0x4 }; + var bytesAtTheEnd = new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5 }; var allocatedMemory = TestProcessMemory!.Allocate(0x1000, false).Value; var targetAddress = allocatedMemory.Range.End - 4; var writeResult = TestProcessMemory.WriteBytes(targetAddress, bytesAtTheEnd, MemoryProtectionStrategy.Ignore); @@ -284,10 +284,10 @@ public void ReadOnTheEdgeOfValidMemoryTest() var buffer = new byte[8]; int byteCount = stream.Read(buffer, 0, 8); - // We should have read only 4 bytes, and the first 4 bytes of the buffer should be the bytes we wrote at the end + // We should have read only 5 bytes, and the first 5 bytes of the buffer should be the bytes we wrote at the end // of our memory segment. - Assert.That(byteCount, Is.EqualTo(4)); - Assert.That(buffer.Take(4), Is.EqualTo(bytesAtTheEnd)); + Assert.That(byteCount, Is.EqualTo(5)); + Assert.That(buffer.Take(5), Is.EqualTo(bytesAtTheEnd)); } /// diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index 299d37f..dff537a 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -79,7 +79,7 @@ public void AllocateWithinRangeTest() [Test] public void AllocateNearAddressTest() { - var nearAddress = new UIntPtr(0x400000000000); + var nearAddress = Is64Bit ? new UIntPtr(0x400000000000) : new UIntPtr(0x40000000); var allocationWithNearAddressResult = TestProcessMemory!.Allocate(0x1000, false, nearAddress: nearAddress); var allocationWithoutNearAddressResult = TestProcessMemory!.Allocate(0x1000, false); Assert.That(allocationWithoutNearAddressResult.IsSuccess, Is.True, allocationWithoutNearAddressResult.ToString()); @@ -224,7 +224,9 @@ public void ReserveTooLargeForAvailableAllocationsTest() [Test] public void ReserveWithLimitRangeTest() { - var range = new MemoryRange(unchecked((UIntPtr)0x400000000000), UIntPtr.MaxValue); + var startAddress = Is64Bit ? unchecked((UIntPtr)0x400000000000) : 0x40000000; + + var range = new MemoryRange(startAddress, UIntPtr.MaxValue); var allocation = TestProcessMemory!.Allocate(0x1000, true).Value; var reservationResult = TestProcessMemory.Reserve(0x1000, true, range); Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); @@ -243,14 +245,17 @@ public void ReserveWithLimitRangeTest() [Test] public void ReserveWithNearAddressTest() { - TestProcessMemory!.Allocate(0x1000, true, - new MemoryRange(unchecked((UIntPtr)0x400000000000), UIntPtr.MaxValue)); + var startAddress1 = Is64Bit ? unchecked((UIntPtr)0x400000000000) : 0x40000000; + var startAddress2 = Is64Bit ? unchecked((UIntPtr)0x200000000000) : 0x20000000; + var startAddress3 = Is64Bit ? unchecked((UIntPtr)0x4B0000000000) : 0x60000000; + + TestProcessMemory!.Allocate(0x1000, true, new MemoryRange(startAddress1, UIntPtr.MaxValue)); var allocation2 = TestProcessMemory!.Allocate(0x1000, true, - new MemoryRange(unchecked((UIntPtr)0x200000000000), UIntPtr.MaxValue)).Value; - TestProcessMemory!.Allocate(0x1000, true, - new MemoryRange(unchecked((UIntPtr)0x4B0000000000), UIntPtr.MaxValue)); + new MemoryRange(startAddress2, UIntPtr.MaxValue)).Value; + TestProcessMemory!.Allocate(0x1000, true, new MemoryRange(startAddress3, UIntPtr.MaxValue)); - var reservationResult = TestProcessMemory.Reserve(0x1000, true, nearAddress:unchecked((UIntPtr)0x2000051C0000)); + var nearAddress = Is64Bit ? unchecked((UIntPtr)0x2000051C0000) : 0x20000500; + var reservationResult = TestProcessMemory.Reserve(0x1000, true, nearAddress: nearAddress); Assert.That(reservationResult.IsSuccess, Is.True, reservationResult.ToString()); var reservation = reservationResult.Value; Assert.That(reservation.ParentAllocation, Is.EqualTo(allocation2)); diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs index b17d192..8ddf109 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryReadTest.cs @@ -230,26 +230,26 @@ public void ReadBytesPartialWithExcessiveLengthTest() /// /// Tests with an address and length that would /// make the read start on a readable address but end on an unreadable one. - /// Expect the result to be 4 (as in 4 bytes read) and the buffer to contain the 4 bytes that were readable. + /// Expect the result to be 5 (as in 5 bytes read) and the buffer to contain the 5 bytes that were readable. /// [Test] public void ReadBytesPartialOnPartiallyUnreadableRangeTest() { // Prepare a segment of memory that is isolated from other memory regions, and has a known sequence of bytes // at the end. - var bytesAtTheEnd = new byte[] { 0x1, 0x2, 0x3, 0x4 }; + var bytesAtTheEnd = new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5 }; var allocatedMemory = TestProcessMemory!.Allocate(0x1000, false).Value; var targetAddress = allocatedMemory.Range.End - 4; var writeResult = TestProcessMemory.WriteBytes(targetAddress, bytesAtTheEnd, MemoryProtectionStrategy.Ignore); Assert.That(writeResult.IsSuccess, Is.True, writeResult.ToString()); - // Read 8 bytes, which should result in reading 4 bytes from the readable region and 4 bytes from the unreadable + // Read 8 bytes, which should result in reading 5 bytes from the readable region and 3 bytes from the unreadable // one. var buffer = new byte[8]; var result = TestProcessMemory.ReadBytesPartial(targetAddress, buffer, 8); Assert.That(result.IsSuccess, Is.True, result.ToString()); - Assert.That(result.Value, Is.EqualTo(4)); - Assert.That(buffer, Is.EqualTo(new byte[] { 0x1, 0x2, 0x3, 0x4, 0, 0, 0, 0 })); + Assert.That(result.Value, Is.EqualTo(5)); + Assert.That(buffer, Is.EqualTo(new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5, 0, 0, 0 })); } /// From 6ce97996e888e235281c1673757793f623026fca Mon Sep 17 00:00:00 2001 From: Doublevil Date: Thu, 5 Jun 2025 21:14:45 +0200 Subject: [PATCH 59/66] Made the docs publication require tests to pass --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 000094f..20353e6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -124,6 +124,7 @@ jobs: publish_docs: if: github.event_name == 'release' runs-on: windows-latest + needs: [run_test] steps: - uses: actions/checkout@v4 From 459246365907460a2f6e6e039840c5f43897bf3d Mon Sep 17 00:00:00 2001 From: Doublevil Date: Thu, 5 Jun 2025 22:24:36 +0200 Subject: [PATCH 60/66] Fixed bitness compatibility check in Write --- src/MindControl/ProcessMemory/ProcessMemory.Write.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs index 7f4ce9f..a911210 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Write.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Write.cs @@ -123,9 +123,9 @@ public Result Write(UIntPtr address, T value, return new IncompatibleBitnessPointerFailure(address); if (value == null) return new InvalidArgumentFailure(nameof(value), "The value to write cannot be null."); - if (value is IntPtr ptr && ptr.ToInt64() > uint.MaxValue) + if (!Is64Bit && value is IntPtr ptr && ptr.ToInt64() > uint.MaxValue) return new IncompatibleBitnessPointerFailure((UIntPtr)ptr); - if (value is UIntPtr uptr && uptr.ToUInt64() > uint.MaxValue) + if (!Is64Bit && value is UIntPtr uptr && uptr.ToUInt64() > uint.MaxValue) return new IncompatibleBitnessPointerFailure(uptr); return value switch From 7a7947ea5d9a19f459b5a39aae7faa40d3ba92ad Mon Sep 17 00:00:00 2001 From: Doublevil Date: Fri, 6 Jun 2025 22:53:50 +0200 Subject: [PATCH 61/66] Changed the allocation range search algorithm --- .../Addressing/PointerExtensions.cs | 18 ++ .../ProcessMemory/ProcessMemory.Allocation.cs | 289 ++++++++++++------ .../ProcessMemoryAllocationTest.cs | 7 +- 3 files changed, 215 insertions(+), 99 deletions(-) diff --git a/src/MindControl/Addressing/PointerExtensions.cs b/src/MindControl/Addressing/PointerExtensions.cs index 15ab048..332503d 100644 --- a/src/MindControl/Addressing/PointerExtensions.cs +++ b/src/MindControl/Addressing/PointerExtensions.cs @@ -40,4 +40,22 @@ public static MemoryRange GetRangeAround(this UIntPtr address, ulong size) ulong end = ulong.MaxValue - addressValue <= halfSize ? ulong.MaxValue : addressValue + halfSize; return new MemoryRange(new UIntPtr(start), new UIntPtr(end)); } + + /// + /// Aligns the given address to the closest boundary of the specified alignment. + /// + /// The address to align. + /// The alignment boundary to align to, must be a power of two. + public static UIntPtr AlignToClosest(this UIntPtr address, uint alignment) + { + ulong addressValue = address.ToUInt64(); + ulong remainder = addressValue % alignment; + + // If already aligned, return as is + if (remainder == 0) + return address; + + // Round up to the next page boundary + return (UIntPtr)(addressValue + (alignment - remainder)); + } } \ No newline at end of file diff --git a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs index 8fd4e16..7d6e10e 100644 --- a/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs +++ b/src/MindControl/ProcessMemory/ProcessMemory.Allocation.cs @@ -70,123 +70,226 @@ internal void Free(MemoryAllocation allocation) /// A result holding either the memory range found, or a failure. /// The reason why the method performs the allocation itself is because we cannot know if the range can /// actually be allocated without performing the allocation. - private Result FindAndAllocateFreeMemory(ulong sizeNeeded, - bool forExecutableCode, MemoryRange? limitRange = null, UIntPtr? nearAddress = null) + private Result FindAndAllocateFreeMemory(ulong sizeNeeded, bool forExecutableCode, + MemoryRange? limitRange = null, UIntPtr? nearAddress = null) { - var maxRange = _osService.GetFullMemoryRange(Is64Bit); - var actualRange = limitRange == null ? maxRange : maxRange.Intersect(limitRange.Value); + // Calculate the required page-aligned size + uint pageSize = _osService.GetPageSize(); + bool isDirectMultiple = sizeNeeded % pageSize == 0; + uint requiredSize = (uint)(sizeNeeded / pageSize + (isDirectMultiple ? (ulong)0 : 1)) * pageSize; + + // With no constraints, let the OS decide where to allocate + if (limitRange == null && nearAddress == null) + { + var allocResult = _osService.AllocateMemory(ProcessHandle, + (int)requiredSize, MemoryAllocationType.Commit | MemoryAllocationType.Reserve, + forExecutableCode ? MemoryProtection.ExecuteReadWrite : MemoryProtection.ReadWrite); + + if (allocResult.IsFailure) + return allocResult.Failure; - // If the given range is not within the process applicative memory, return null - if (actualRange == null) + return MemoryRange.FromStartAndSize(allocResult.Value, requiredSize); + } + + // Define the search boundaries + var maxRange = _osService.GetFullMemoryRange(Is64Bit); + var searchRange = limitRange == null ? maxRange : maxRange.Intersect(limitRange.Value); + + if (searchRange == null) return new LimitRangeOutOfBoundsFailure(maxRange); - // Compute the minimum multiple of the system page size that can fit the size needed - // This will be the maximum size that we are going to allocate - uint pageSize = _osService.GetPageSize(); - bool isDirectMultiple = sizeNeeded % pageSize == 0; - uint minFittingPageSize = (uint)(sizeNeeded / pageSize + (isDirectMultiple ? (ulong)0 : 1)) * pageSize; + // Determine the starting address for the search + UIntPtr startAddress = nearAddress ?? searchRange.Value.Start; + + // For near address searches, we'll try both directions + if (nearAddress != null) + { + // Try to allocate memory close to the requested address + var result = TryAllocateNear(startAddress, requiredSize, pageSize, forExecutableCode, searchRange.Value); + if (result.IsSuccess) + return result; + } + else + { + // When no near address is specified, just scan forward from start to end + return AllocateNextFreeMemoryRange(startAddress, searchRange.Value.End, requiredSize, pageSize, forExecutableCode); + } + + return new NoFreeMemoryFailure(searchRange.Value, startAddress); + } + + /// + /// Attempts to allocate a memory range as close as possible to the specified target address. + /// + /// Target address to allocate memory near. + /// Size of the memory range required. + /// Size of the memory pages to use for allocation. This is used to align the addresses. + /// + /// Set to true if the memory range can be used to store executable code. + /// Range that defines the boundaries of the free memory search. + private Result TryAllocateNear(UIntPtr targetAddress, ulong requiredSize, uint pageSize, + bool forExecutableCode, MemoryRange searchRange) + { + // Try to allocate at the target address first, just in case + var directResult = TryAllocateAt(targetAddress, requiredSize, forExecutableCode); + if (directResult.IsSuccess) + return directResult; + + var targetRegion = _osService.GetRegionMetadata(ProcessHandle, targetAddress).ValueOrDefault(); - // Browse through regions in the memory range to find the first one that satisfies the size needed - // If a near address is specified, start there. Otherwise, start at the beginning of the memory range. - // For near address search, we are going to search back and forth around the address, which complicates the - // process a bit. It means we have to keep track of both the next lowest address and next highest address. - var nextAddress = nearAddress ?? actualRange.Value.Start; - var nextRegionStartAddress = nextAddress; - var previousRegionEndAddress = nextAddress; - var highestAddressAttempted = nextAddress; - var lowestAddressAttempted = nextAddress; - bool goingForward = true; + UIntPtr highAddress = targetAddress.AlignToClosest(pageSize); + MemoryRangeMetadata highRegion = highAddress < targetRegion.StartAddress + targetRegion.Size ? + targetRegion : GetNextFreeRegion(targetRegion, searchRange, goForward: true).ValueOrDefault(); + UIntPtr lowAddress = highAddress < pageSize ? 0 : highAddress - pageSize; + MemoryRangeMetadata lowRegion = lowAddress >= targetRegion.StartAddress ? + targetRegion : GetNextFreeRegion(targetRegion, searchRange, goForward: false).ValueOrDefault(); - MemoryRange? freeRange = null; - MemoryRangeMetadata currentMetadata; - while (nextAddress.ToUInt64() <= actualRange.Value.End.ToUInt64() - && nextAddress.ToUInt64() >= actualRange.Value.Start.ToUInt64() - && (currentMetadata = _osService.GetRegionMetadata(ProcessHandle, nextAddress) - .ValueOrDefault()).Size.ToUInt64() > 0) + // Track whether we've exhausted either direction + bool canGoHigher = highRegion.Size != 0 && highAddress < searchRange.End; + bool canGoLower = lowRegion.Size != 0 && lowAddress >= searchRange.Start; + + while (canGoHigher || canGoLower) { - nextRegionStartAddress = (UIntPtr)Math.Max(nextRegionStartAddress.ToUInt64(), - nextAddress.ToUInt64() + currentMetadata.Size.ToUInt64()); - previousRegionEndAddress = (UIntPtr)Math.Min(previousRegionEndAddress.ToUInt64(), - currentMetadata.StartAddress.ToUInt64() - 1); - highestAddressAttempted = Math.Max(highestAddressAttempted, nextAddress); - lowestAddressAttempted = Math.Min(lowestAddressAttempted, nextAddress); - - var currentAddress = nextAddress; - - // If the current region cannot be used, reinitialize the current free range and keep iterating - if (!currentMetadata.IsFree) + // Try above the target (increasing addresses) if the high address is closer + if (canGoHigher && + (!canGoLower || targetAddress.DistanceTo(highAddress) <= targetAddress.DistanceTo(lowAddress))) { - freeRange = null; + var result = TryAllocateAt(highAddress, requiredSize, forExecutableCode); + if (result.IsSuccess) + return result; - // In a near address search, we may change direction there depending on which next address is closest. - if (nearAddress != null) + highAddress += pageSize; + + if (highAddress >= highRegion.StartAddress + highRegion.Size) { - var forwardDistance = nearAddress.Value.DistanceTo(nextRegionStartAddress); - var backwardDistance = nearAddress.Value.DistanceTo(previousRegionEndAddress); - goingForward = forwardDistance <= backwardDistance - && nextRegionStartAddress.ToUInt64() <= actualRange.Value.End.ToUInt64(); + var nextRegion = GetNextFreeRegion(highRegion, searchRange, goForward: true); + if (nextRegion.IsFailure) + canGoHigher = false; + else + { + highRegion = nextRegion.Value; + highAddress = highRegion.StartAddress; + } } - // Travel to the next region - nextAddress = goingForward ? nextRegionStartAddress : previousRegionEndAddress; - continue; + if (highAddress >= searchRange.End) + canGoHigher = false; } - // Build a range with the current region - // Extend the free range if it's not null, so that we can have ranges that span across multiple regions. - if (goingForward) - { - freeRange = new MemoryRange(freeRange?.Start ?? currentAddress, - (UIntPtr)(currentMetadata.StartAddress.ToUInt64() + currentMetadata.Size.ToUInt64() - 1)); - } - else + // Try below the target (decreasing addresses) if the low address is closer + if (canGoLower && + (!canGoHigher || targetAddress.DistanceTo(lowAddress) <= targetAddress.DistanceTo(highAddress))) { - freeRange = new MemoryRange(currentMetadata.StartAddress, - (UIntPtr)(freeRange?.End.ToUInt64() ?? currentMetadata.StartAddress.ToUInt64() - + currentMetadata.Size.ToUInt64() - 1)); - } - - if (freeRange.Value.GetSize() >= sizeNeeded) - { - // The free range is large enough. - // If the free range is larger than the size needed, we will allocate the minimum multiple of the - // system page size that can fit the requested size. - ulong neededSize = Math.Min(freeRange.Value.GetSize(), minFittingPageSize); - var finalRange = MemoryRange.FromStartAndSize(freeRange.Value.Start, neededSize); - - // Even if they are free, some regions cannot be allocated. - // The only way to know if a region can be allocated is to try to allocate it. - var allocateResult = _osService.AllocateMemory(ProcessHandle, finalRange.Start, - (int)finalRange.GetSize(), MemoryAllocationType.Commit | MemoryAllocationType.Reserve, - forExecutableCode ? MemoryProtection.ExecuteReadWrite : MemoryProtection.ReadWrite); + var result = TryAllocateAt(lowAddress, requiredSize, forExecutableCode); + if (result.IsSuccess) + return result; - // If the allocation succeeded, return the range. - if (allocateResult.IsSuccess) - return finalRange; + lowAddress -= pageSize; - // The allocation failed. Reset the current range and keep iterating. - freeRange = null; - - // In a near address search, we may change direction there depending on which next address is closest. - var nextAddressForward = highestAddressAttempted + pageSize; - var nextAddressBackward = lowestAddressAttempted - pageSize; - if (nearAddress != null) + if (lowAddress < lowRegion.StartAddress) { - var forwardDistance = nearAddress.Value.DistanceTo(nextAddressForward); - var backwardDistance = nearAddress.Value.DistanceTo(nextAddressBackward); - goingForward = forwardDistance <= backwardDistance - && nextRegionStartAddress.ToUInt64() <= actualRange.Value.End.ToUInt64(); + var nextRegion = GetNextFreeRegion(lowRegion, searchRange, goForward: false); + if (nextRegion.IsFailure) + canGoLower = false; + else + { + lowRegion = nextRegion.Value; + lowAddress = lowRegion.StartAddress + lowRegion.Size - pageSize; + } } - // Travel one page size forward or backward - nextAddress = goingForward ? nextAddressForward : nextAddressBackward; - + if (lowAddress < searchRange.Start) + canGoLower = false; + } + } + + return new NoFreeMemoryFailure(searchRange, targetAddress); + } + + /// + /// Starting at the given region, searches for the next free memory region within the specified search range. + /// + /// Starting region to search from. + /// Range of memory to search for free regions. + /// Set to true to search forward from the start region, or false to search backward. + /// + private Result GetNextFreeRegion(MemoryRangeMetadata startRegion, MemoryRange searchRange, + bool goForward) + { + MemoryRangeMetadata currentRegion = startRegion; + while (currentRegion.Size != 0 && currentRegion.StartAddress.ToUInt64() < searchRange.End.ToUInt64()) + { + var nextAddress = goForward + ? currentRegion.StartAddress + currentRegion.Size + : currentRegion.StartAddress - 1; + + currentRegion = _osService.GetRegionMetadata(ProcessHandle, nextAddress).ValueOrDefault(); + + if (currentRegion.IsFree) + return currentRegion; + } + + return new NoFreeMemoryFailure(searchRange, searchRange.End); + } + + /// + /// Allocates the next free memory range within the specified start and end addresses that can hold the + /// required size. + /// + /// Address to start searching for a free memory range. + /// Address to end searching for a free memory range. + /// Size of the memory range required. + /// Size of the memory pages to use for allocation. This is used to align the addresses. + /// + /// Set to true if the memory range can be used to store executable code. + private Result AllocateNextFreeMemoryRange(UIntPtr startAddress, UIntPtr endAddress, + ulong requiredSize, uint pageSize, bool forExecutableCode) + { + var currentAddress = startAddress; + + while (currentAddress.ToUInt64() <= endAddress.ToUInt64()) + { + var metadata = _osService.GetRegionMetadata(ProcessHandle, currentAddress).ValueOrDefault(); + + // If we got invalid metadata or reached the end + if (metadata.Size == 0) + break; + + // Check if the region is free + if (metadata.IsFree && metadata.Size >= requiredSize) + { + var finalRange = MemoryRange.FromStartAndSize(metadata.StartAddress, requiredSize); + var result = TryAllocateAt(finalRange.Start, requiredSize, forExecutableCode); + if (result.IsSuccess) + return result; + + currentAddress += pageSize; continue; } + + // Move to the next region + currentAddress = metadata.StartAddress + metadata.Size; } + + return new NoFreeMemoryFailure(new MemoryRange(startAddress, endAddress), startAddress); + } - // We reached the end of the memory range and didn't find a suitable free range. - return new NoFreeMemoryFailure(actualRange.Value, nextAddress); + /// + /// Attempts to allocate a memory range at the specified address with the given size. + /// + /// The address to allocate memory at. + /// Size of the memory range to allocate. + /// Set to true if the memory range can be used to store executable code. + private Result TryAllocateAt(UIntPtr address, ulong size, bool forExecutableCode) + { + var allocResult = _osService.AllocateMemory(ProcessHandle, address, + (int)size, MemoryAllocationType.Commit | MemoryAllocationType.Reserve, + forExecutableCode ? MemoryProtection.ExecuteReadWrite : MemoryProtection.ReadWrite); + + if (allocResult.IsSuccess) + return MemoryRange.FromStartAndSize(address, size); + + return allocResult.Failure; } #region Store diff --git a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs index dff537a..0fcdedc 100644 --- a/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs +++ b/test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAllocationTest.cs @@ -55,19 +55,14 @@ public void AllocateExecutableTest() /// /// Tests the method. - /// Performs two allocations: one with a range and one without a range. - /// The range starts after the expected range of the rangeless allocation. - /// Expect the rangeless allocation to be outside of the range, and the ranged allocation to be within the range. + /// Performs an allocation within a specific range and verifies that the resulting allocation is within that range. /// [Test] public void AllocateWithinRangeTest() { var range = new MemoryRange(new UIntPtr(0x120000), UIntPtr.MaxValue); - var allocationWithoutRangeResult = TestProcessMemory!.Allocate(0x1000, false); var allocationWithRangeResult = TestProcessMemory!.Allocate(0x1000, false, range); - Assert.That(allocationWithoutRangeResult.IsSuccess, Is.True, allocationWithoutRangeResult.ToString()); Assert.That(allocationWithRangeResult.IsSuccess, Is.True, allocationWithRangeResult.ToString()); - Assert.That(range.Contains(allocationWithoutRangeResult.Value.Range), Is.False); Assert.That(range.Contains(allocationWithRangeResult.Value.Range), Is.True); } From e1d3c7dccc47faf8235096f363b9d45715ac1f2c Mon Sep 17 00:00:00 2001 From: Doublevil Date: Fri, 6 Jun 2025 23:52:13 +0200 Subject: [PATCH 62/66] Updated docs and MindControl.Code publish yml --- .github/workflows/publish.yml | 9 ++- README.md | 4 +- src/MindControl.Code/MindControl.Code.csproj | 58 ++++++++++---------- src/MindControl/MindControl.csproj | 2 +- src/MindControl/README.md | 14 +++-- 5 files changed, 48 insertions(+), 39 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 20353e6..4b8de37 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,10 +36,13 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 - # Create the NuGet package in the folder from the environment variable NuGetDirectory - - run: dotnet pack src/MindControl/MindControl.csproj --configuration Release --output ${{ env.NuGetDirectory }} + # Create the NuGet packages in the folder from the environment variable NuGetDirectory + - name: Create NuGet packages + run: | + dotnet pack src/MindControl/MindControl.csproj --configuration Release --output ${{ env.NuGetDirectory }} + dotnet pack src/MindControl.Code/MindControl.Code.csproj --configuration Release --output ${{ env.NuGetDirectory }} - # Publish the NuGet package as an artifact, so they can be used in the following jobs + # Publish the NuGet packages as an artifact, so they can be used in the following jobs - uses: actions/upload-artifact@v4 with: name: nuget diff --git a/README.md b/README.md index e16c5d8..2d663ae 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # MindControl +[![NuGet Version](https://img.shields.io/nuget/v/mindcontrol?style=for-the-badge)](https://www.nuget.org/packages/mindcontrol)[![Downloads](https://img.shields.io/nuget/dt/mindcontrol?style=for-the-badge)](https://www.nuget.org/packages/mindcontrol)[![License](https://img.shields.io/github/license/Doublevil/mind-control?style=for-the-badge)](https://github.com/Doublevil/mind-control/blob/main/LICENSE) + MindControl is a .net hacking library for Windows that allows you to manipulate a game or any other process and its internal memory. > **DO NOT use this library to cheat in online competitive games. Cheaters ruins the fun for everyone. If you ignore this warning, I will do my best to shut down your project.** @@ -26,7 +28,7 @@ UIntPtr targetAddress = myGame.FindBytes("4D 79 ?? ?? ?? ?? ?? ?? 56 61 6C 75 65 // ... And many more features ``` -See [the documentation](doc/GetStarted.md) to get started, whether you already dabble in memory hacking or are completely new to it. +See [the documentation](https://doublevil.github.io/mind-control/guide/introduction.html) to get started, whether you already dabble in memory hacking or are completely new to it. ## Features diff --git a/src/MindControl.Code/MindControl.Code.csproj b/src/MindControl.Code/MindControl.Code.csproj index 6cc1645..f8a58ae 100644 --- a/src/MindControl.Code/MindControl.Code.csproj +++ b/src/MindControl.Code/MindControl.Code.csproj @@ -1,35 +1,33 @@  - - - net8.0 - enable - enable - MindControl - MindControl.Code - https://github.com/Doublevil/mind-control - git - 1.0.0-alpha-25053101 - - - - - true - True - Doublevil - An extension library for MindControl that gives access to advanced ASM code-related features like - hooks. - library, process, memory, win32, game, hacking, hack, trainer, cheat, cheating - MIT - icon.png - README.md - - - - + + net8.0 + enable + enable + MindControl + MindControl.Code + https://github.com/Doublevil/mind-control + git + 1.0.0-alpha-25060601 + - - - + + + true + True + Doublevil + An extension library for MindControl that gives access to advanced code-related features like hooks. + library, process, memory, win32, game, hacking, hack, trainer, cheat, cheating + MIT + icon.png + README.md + + + + + + + + diff --git a/src/MindControl/MindControl.csproj b/src/MindControl/MindControl.csproj index d39e715..b47972b 100644 --- a/src/MindControl/MindControl.csproj +++ b/src/MindControl/MindControl.csproj @@ -7,7 +7,7 @@ MindControl https://github.com/Doublevil/mind-control git - 1.0.0-alpha-25053101 + 1.0.0-alpha-25060601 diff --git a/src/MindControl/README.md b/src/MindControl/README.md index fe0793d..00c09db 100644 --- a/src/MindControl/README.md +++ b/src/MindControl/README.md @@ -9,15 +9,21 @@ MindControl is a .net hacking library for Windows that allows you to manipulate Here is a quick example to get you started. ```csharp -var myGame = ProcessMemory.OpenProcess("mygame"); // A process with this name must be running +var myGame = ProcessMemory.OpenProcess("mygame").Result; // A process with this name must be running var hpAddress = new PointerPath("mygame.exe+1D005A70,1C,8"); // See the docs for how to determine these // Read values -int currentHp = myGame.ReadInt(hpAddress); +var currentHp = myGame.Read(hpAddress); Console.WriteLine($"You have {currentHp}HP"); // Example output: "You have 50HP" // Write values -myGame.WriteInt(hpAddress, 9999); +myGame.Write(hpAddress, 9999); + +// Find the first occurrence of a pattern in memory, with wildcard bytes +UIntPtr targetAddress = myGame.FindBytes("4D 79 ?? ?? ?? ?? ?? ?? 56 61 6C 75 65") + .FirstOrDefault(); + +// ... And many more features ``` -See [the full documentation on GitHub](https://github.com/Doublevil/mind-control) for more info. \ No newline at end of file +See [the documentation](https://doublevil.github.io/mind-control/guide/introduction.html) to get started, whether you already dabble in memory hacking or are completely new to it. \ No newline at end of file From 0acccb6963aebce36bcc4e4a2f43074b4034f4ad Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 7 Jun 2025 10:14:12 +0200 Subject: [PATCH 63/66] Added missing icon and readme for MindControl.Code --- res/icon_code.png | Bin 0 -> 17668 bytes res/icon_code.svg | 66 +++++++++++++++++++ src/MindControl.Code/MindControl.Code.csproj | 5 ++ src/MindControl.Code/README.md | 31 +++++++++ src/MindControl.Code/res/icon.png | Bin 0 -> 17668 bytes 5 files changed, 102 insertions(+) create mode 100644 res/icon_code.png create mode 100644 res/icon_code.svg create mode 100644 src/MindControl.Code/README.md create mode 100644 src/MindControl.Code/res/icon.png diff --git a/res/icon_code.png b/res/icon_code.png new file mode 100644 index 0000000000000000000000000000000000000000..539294b600dcf93675df5d7c57e3983d7c97a113 GIT binary patch literal 17668 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 iRi*z{*By10ng0jVAoeCH{wG`j0000 + + + + + + + + + + + + + + diff --git a/src/MindControl.Code/MindControl.Code.csproj b/src/MindControl.Code/MindControl.Code.csproj index f8a58ae..88e52c6 100644 --- a/src/MindControl.Code/MindControl.Code.csproj +++ b/src/MindControl.Code/MindControl.Code.csproj @@ -27,6 +27,11 @@ + + + + + diff --git a/src/MindControl.Code/README.md b/src/MindControl.Code/README.md new file mode 100644 index 0000000..a93d397 --- /dev/null +++ b/src/MindControl.Code/README.md @@ -0,0 +1,31 @@ +# MindControl.Code + +MindControl is a .net hacking library for Windows that allows you to manipulate a game or any other process and its internal memory. + +This library is an extension of the main [MindControl](https://www.nuget.org/packages/MindControl) package, adding features related to code manipulation. + +> **DO NOT use this library to cheat in online competitive games. Cheaters ruins the fun for everyone. If you ignore this warning, I will do my best to shut down your project.** + +## Getting started + +Here is a quick example to get you started. + +```csharp +var myGame = ProcessMemory.OpenProcess("mygame").Result; // A process with this name must be running + +// Write some assembly code, for example with Iced.Intel (a byte[] also works) +var assembler = new Assembler(64); +assembler.mov(rcx, value); +// ... + +// Insert the code at the address "mygame.exe+0168EEA0" +CodeChange codeInjection = processMemory.InsertCodeAt("mygame.exe+0168EEA0", assembler).Value; +// Original code is untouched, your code will be executed right before the instruction at that address (through a hook). + +// Disposing the CodeChange object restores the original code +codeInjection.Dispose(); + +// Check out the docs for more features! +``` + +See [the documentation](https://doublevil.github.io/mind-control/guide/introduction.html) to get started, whether you already dabble in memory hacking or are completely new to it. \ No newline at end of file diff --git a/src/MindControl.Code/res/icon.png b/src/MindControl.Code/res/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..539294b600dcf93675df5d7c57e3983d7c97a113 GIT binary patch literal 17668 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 iRi*z{*By10ng0jVAoeCH{wG`j0000 Date: Sat, 7 Jun 2025 10:20:21 +0200 Subject: [PATCH 64/66] Added DotNet.ReproducibleBuilds to MindControl.Code --- src/MindControl.Code/MindControl.Code.csproj | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/MindControl.Code/MindControl.Code.csproj b/src/MindControl.Code/MindControl.Code.csproj index 88e52c6..744cfbb 100644 --- a/src/MindControl.Code/MindControl.Code.csproj +++ b/src/MindControl.Code/MindControl.Code.csproj @@ -35,4 +35,11 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + From 7f70891890d24048679870372b77296c9f973eec Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 7 Jun 2025 10:42:38 +0200 Subject: [PATCH 65/66] Fixed a value watcher test --- test/MindControl.Test/AnchorTests/ValueWatcherTest.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/MindControl.Test/AnchorTests/ValueWatcherTest.cs b/test/MindControl.Test/AnchorTests/ValueWatcherTest.cs index a621e9f..e548cf6 100644 --- a/test/MindControl.Test/AnchorTests/ValueWatcherTest.cs +++ b/test/MindControl.Test/AnchorTests/ValueWatcherTest.cs @@ -185,7 +185,7 @@ public void ValueReacquiredTest() [Test] public void ValueReacquiredAndChangedTest() { - using var reservation = TestProcessMemory!.Reserve(16, false).Value; + using var reservation = TestProcessMemory!.Reserve(24, false).Value; TestProcessMemory.Write(reservation.Address, reservation.Address + 8).ThrowOnFailure(); int targetValue = 46; TestProcessMemory.Write(reservation.Address + 8, targetValue).ThrowOnFailure(); @@ -221,7 +221,8 @@ public void ValueReacquiredAndChangedTest() watcher.UpdateState(); // This should raise a ValueLost event // Make the pointer path resolve to an area with a 0 value - TestProcessMemory.Write(reservation.Address, reservation.Address + 12).ThrowOnFailure(); + TestProcessMemory.Write(reservation.Address + 16, new byte[8]).ThrowOnFailure(); + TestProcessMemory.Write(reservation.Address, reservation.Address + 16).ThrowOnFailure(); watcher.UpdateState(); // This should raise a ValueReacquired event and then a ValueChanged event watcher.Dispose(); From 5263d0285a8bf9e4afb12f04ffc2a14e2194e768 Mon Sep 17 00:00:00 2001 From: Doublevil Date: Sat, 7 Jun 2025 11:00:10 +0200 Subject: [PATCH 66/66] Changed version number to 1.0.0 --- src/MindControl.Code/MindControl.Code.csproj | 2 +- src/MindControl/MindControl.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MindControl.Code/MindControl.Code.csproj b/src/MindControl.Code/MindControl.Code.csproj index 744cfbb..aa87e44 100644 --- a/src/MindControl.Code/MindControl.Code.csproj +++ b/src/MindControl.Code/MindControl.Code.csproj @@ -8,7 +8,7 @@ MindControl.Code https://github.com/Doublevil/mind-control git - 1.0.0-alpha-25060601 + 1.0.0 diff --git a/src/MindControl/MindControl.csproj b/src/MindControl/MindControl.csproj index b47972b..4c12319 100644 --- a/src/MindControl/MindControl.csproj +++ b/src/MindControl/MindControl.csproj @@ -7,7 +7,7 @@ MindControl https://github.com/Doublevil/mind-control git - 1.0.0-alpha-25060601 + 1.0.0