using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ReClassNET.Extensions;
using ReClassNET.Memory;
using ReClassNET.MemoryScanner.Comparer;
using ReClassNET.Util;
namespace ReClassNET.MemoryScanner
{
public class Scanner : IDisposable
{
///
/// Helper class for consolidated memory regions.
///
private class ConsolidatedMemoryRegion
{
public IntPtr Address { get; set; }
public int Size { get; set; }
}
private readonly RemoteProcess process;
private readonly CircularBuffer stores;
public ScanSettings Settings { get; }
private ScanResultStore CurrentStore => stores.Head;
///
/// Gets the total result count from the last scan.
///
public int TotalResultCount => CurrentStore?.TotalResultCount ?? 0;
///
/// Checks if the last scan can be undone.
///
public bool CanUndoLastScan => stores.Count > 1;
private bool isFirstScan;
public Scanner(RemoteProcess process, ScanSettings settings)
{
Contract.Requires(process != null);
Contract.Requires(settings != null);
stores = new CircularBuffer(3);
this.process = process;
Settings = settings;
isFirstScan = true;
}
public void Dispose()
{
foreach (var store in stores)
{
store?.Dispose();
}
stores.Clear();
}
///
/// Retrieves the results of the last scan from the store.
///
///
/// An enumeration of the s of the last scan.
///
public IEnumerable GetResults()
{
Contract.Ensures(Contract.Result>() != null);
if (CurrentStore == null)
{
return Enumerable.Empty();
}
return CurrentStore.GetResultBlocks().SelectMany(rb => rb.Results.Select(r =>
{
// Convert the block offset to a real address.
var scanResult = r.Clone();
scanResult.Address = scanResult.Address.Add(rb.Start);
return scanResult;
}));
}
///
/// Restores the results of the previous scan.
///
/// Thrown if no previous results are present.
public void UndoLastScan()
{
if (!CanUndoLastScan)
{
throw new InvalidOperationException();
}
var store = stores.Dequeue();
store?.Dispose();
}
///
/// Creates a new and uses the system temporary path as file location.
///
/// The new .
private ScanResultStore CreateStore()
{
return new ScanResultStore(Settings.ValueType, Path.GetTempPath());
}
///
/// Gets a list of the sections which meet the provided scan settings.
///
/// A list of searchable sections.
private IList GetSearchableSections()
{
Contract.Ensures(Contract.Result>() != null);
return process.Sections
.Where(s => !s.Protection.HasFlag(SectionProtection.Guard))
.Where(s => s.Start.IsInRange(Settings.StartAddress, Settings.StopAddress)
|| Settings.StartAddress.IsInRange(s.Start, s.End)
|| Settings.StopAddress.IsInRange(s.Start, s.End))
.Where(s => s.Type switch
{
SectionType.Private => Settings.ScanPrivateMemory,
SectionType.Image => Settings.ScanImageMemory,
SectionType.Mapped => Settings.ScanMappedMemory,
_ => false
})
.Where(s =>
{
var isWritable = s.Protection.HasFlag(SectionProtection.Write);
return Settings.ScanWritableMemory switch
{
SettingState.Yes => isWritable,
SettingState.No => !isWritable,
_ => true
};
})
.Where(s =>
{
var isExecutable = s.Protection.HasFlag(SectionProtection.Execute);
return Settings.ScanExecutableMemory switch
{
SettingState.Yes => isExecutable,
SettingState.No => !isExecutable,
_ => true
};
})
.Where(s =>
{
var isCopyOnWrite = s.Protection.HasFlag(SectionProtection.CopyOnWrite);
return Settings.ScanCopyOnWriteMemory switch
{
SettingState.Yes => isCopyOnWrite,
SettingState.No => !isCopyOnWrite,
_ => true
};
})
.ToList();
}
///
/// Starts an async search with the provided .
/// The results are stored in the store.
///
/// The comparer to scan for values.
/// The object to report the current progress.
/// The to stop the scan.
/// The asynchronous result indicating if the scan completed.
public Task Search(IScanComparer comparer, IProgress progress, CancellationToken ct)
{
return isFirstScan ? FirstScan(comparer, progress, ct) : NextScan(comparer, progress, ct);
}
///
/// Starts an async first scan with the provided .
///
/// The comparer to scan for values.
/// The object to report the current progress.
/// The to stop the scan.
/// The asynchronous result indicating if the scan completed.
private Task FirstScan(IScanComparer comparer, IProgress progress, CancellationToken ct)
{
Contract.Requires(comparer != null);
Contract.Ensures(Contract.Result>() != null);
var store = CreateStore();
var sections = GetSearchableSections();
if (sections.Count == 0)
{
return Task.FromResult(true);
}
var regions = ConsolidateSections(sections);
var initialBufferSize = (int)(regions.Average(s => s.Size) + 1);
progress?.Report(0);
var counter = 0;
var totalSectionCount = (float)regions.Count;
return Task.Run(() =>
{
// Algorithm:
// 1. Partition the sections for the worker threads.
// 2. Create a ScannerContext per worker thread.
// 3. n Worker -> m Sections: Read data, search results, store results
var result = Parallel.ForEach(
regions, // Sections get grouped by the framework to balance the workers.
() => new ScannerContext(CreateWorker(Settings, comparer), initialBufferSize), // Create a new context for every worker (thread).
(s, state, _, context) =>
{
if (!ct.IsCancellationRequested)
{
var start = s.Address;
var end = s.Address + s.Size;
var size = s.Size;
if (Settings.StartAddress.IsInRange(start, end))
{
size -= Settings.StartAddress.Sub(start).ToInt32();
start = Settings.StartAddress;
}
if (Settings.StopAddress.IsInRange(start, end))
{
size -= end.Sub(Settings.StopAddress).ToInt32();
}
context.EnsureBufferSize(size);
var buffer = context.Buffer;
if (process.ReadRemoteMemoryIntoBuffer(start, ref buffer, 0, size)) // Fill the buffer.
{
var results = context.Worker.Search(buffer, size, ct) // Search for results.
.OrderBy(r => r.Address, IntPtrComparer.Instance)
.ToList();
if (results.Count > 0)
{
var block = CreateResultBlock(results, start);
store.AddBlock(block); // Store the result block.
}
}
progress?.Report((int)(Interlocked.Increment(ref counter) / totalSectionCount * 100));
}
else
{
state.Stop();
}
return context;
},
w => { }
);
store.Finish();
var previousStore = stores.Enqueue(store);
previousStore?.Dispose();
isFirstScan = false;
return result.IsCompleted;
}, ct);
}
///
/// Starts an async next scan with the provided .
/// The next scan uses the previous results to refine the results.
///
/// The comparer to scan for values.
/// The object to report the current progress.
/// The to stop the scan.
/// The asynchronous result indicating if the scan completed.
private Task NextScan(IScanComparer comparer, IProgress progress, CancellationToken ct)
{
Contract.Requires(comparer != null);
Contract.Ensures(Contract.Result>() != null);
var store = CreateStore();
progress?.Report(0);
var counter = 0;
var totalResultCount = (float)CurrentStore.TotalResultCount;
return Task.Run(() =>
{
var result = Parallel.ForEach(
CurrentStore.GetResultBlocks(),
() => new ScannerContext(CreateWorker(Settings, comparer), 0),
(b, state, _, context) =>
{
if (!ct.IsCancellationRequested)
{
context.EnsureBufferSize(b.Size);
var buffer = context.Buffer;
if (process.ReadRemoteMemoryIntoBuffer(b.Start, ref buffer, 0, b.Size))
{
var results = context.Worker.Search(buffer, buffer.Length, b.Results, ct)
.OrderBy(r => r.Address, IntPtrComparer.Instance)
.ToList();
if (results.Count > 0)
{
var block = CreateResultBlock(results, b.Start);
store.AddBlock(block);
}
}
progress?.Report((int)(Interlocked.Add(ref counter, b.Results.Count) / totalResultCount * 100));
}
else
{
state.Stop();
}
return context;
},
w => { }
);
store.Finish();
var previousStore = stores.Enqueue(store);
previousStore?.Dispose();
return result.IsCompleted;
}, ct);
}
///
/// Consolidate memory sections which are direct neighbours to reduce the number of work items.
///
/// A list of sections.
/// A list of consolidated memory regions.
private static List ConsolidateSections(IList sections)
{
var regions = new List();
if (sections.Count > 0)
{
var address = sections[0].Start;
var size = sections[0].Size.ToInt32();
for (var i = 1; i < sections.Count; ++i)
{
var section = sections[i];
if (address + size != section.Start)
{
regions.Add(new ConsolidatedMemoryRegion { Address = address, Size = size });
address = section.Start;
size = section.Size.ToInt32();
}
else
{
size += section.Size.ToInt32();
}
}
regions.Add(new ConsolidatedMemoryRegion { Address = address, Size = size });
}
return regions;
}
///
/// Creates a result block from the scan results and adjusts the result offset.
///
/// The results in this block.
/// The start address of the previous block or section.
/// The new result block.
private static ScanResultBlock CreateResultBlock(IReadOnlyList results, IntPtr previousStartAddress)
{
var firstResult = results.First();
var lastResult = results.Last();
// Calculate start and end address
var startAddress = firstResult.Address.Add(previousStartAddress);
var endAddress = lastResult.Address.Add(previousStartAddress) + lastResult.ValueSize;
// Adjust the offsets of the results
var firstOffset = firstResult.Address;
foreach (var result in results)
{
result.Address = result.Address.Sub(firstOffset);
}
var block = new ScanResultBlock(
startAddress,
endAddress,
results
);
return block;
}
private static IScannerWorker CreateWorker(ScanSettings settings, IScanComparer comparer)
{
if (comparer is ISimpleScanComparer simpleScanComparer)
{
return new SimpleScannerWorker(settings, simpleScanComparer);
}
if (comparer is IComplexScanComparer complexScanComparer)
{
return new ComplexScannerWorker(settings, complexScanComparer);
}
throw new Exception();
}
}
}