Blocking fixes: 1. Read<T> now returns default(T) on failed/partial read instead of crash (applies to single Read<T>, array Read<T>, and ReadBytes) 2. InProcessReader uses ReadProcessMemory via handle instead of unsafe Buffer.MemoryCopy — fails soft on bad address instead of AV'ing 3. GetRelative now returns absolute - ImageBase (inverse of GetAbsolute). Fix round-trip test to validate at arbitrary offsets, not just ImageBase 4. ReadString reads in 64-byte chunks with encoding-aware null-terminator pattern matching (handles UTF-16's 2-byte null, UTF-32's 4-byte null) Cleanup: 5. InProcessReader validates handle on open and uses RPM through it (handle is no longer unused) 6. Array marshal read: pin raw buffer once, PtrToStructure at offset 7. StructureToByteArray(Span) delegates to byte[] overload, no duplicate New tests: 4 invalid-address grace tests (returns default/empty/false). All 57 passing.
91 lines
2.6 KiB
C#
91 lines
2.6 KiB
C#
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using WhiteMagic.Native;
|
|
|
|
namespace WhiteMagic;
|
|
|
|
/// <summary>
|
|
/// In-process memory reader that accesses the owning process's memory through
|
|
/// <see cref="NativeMethods.ReadProcessMemory"/> and
|
|
/// <see cref="NativeMethods.WriteProcessMemory"/> on a handle to the current
|
|
/// process. Unlike the unsafe-deref approach, this fails softly (returns
|
|
/// empty / zero bytes) on invalid or protected addresses instead of crashing
|
|
/// the host process with an <see cref="AccessViolationException"/>.
|
|
/// </summary>
|
|
public sealed class InProcessReader : MemoryBase
|
|
{
|
|
private readonly SafeMemoryHandle _handle;
|
|
private readonly IntPtr _imageBase;
|
|
private bool _disposed;
|
|
|
|
/// <summary>
|
|
/// Creates an in-process reader for the current process.
|
|
/// </summary>
|
|
public InProcessReader()
|
|
{
|
|
Process current = Process.GetCurrentProcess();
|
|
_handle = NativeMethods.OpenProcess(
|
|
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation,
|
|
false,
|
|
current.Id);
|
|
if (_handle.IsInvalid)
|
|
{
|
|
int error = Marshal.GetLastPInvokeError();
|
|
throw new InvalidOperationException(
|
|
$"OpenProcess failed for PID {current.Id}: error {error}");
|
|
}
|
|
|
|
_imageBase = current.MainModule?.BaseAddress ?? IntPtr.Zero;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override IntPtr ImageBase => _imageBase;
|
|
|
|
/// <inheritdoc />
|
|
public override SafeMemoryHandle Handle => _handle;
|
|
|
|
/// <inheritdoc />
|
|
public override byte[] ReadBytes(IntPtr address, int count, bool isRelative = false)
|
|
{
|
|
if (isRelative)
|
|
address = GetAbsolute(address);
|
|
|
|
byte[] buffer = new byte[count];
|
|
if (!NativeMethods.ReadProcessMemory(_handle, address, buffer, count, out nint bytesRead))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
if ((int)bytesRead != count)
|
|
{
|
|
Array.Resize(ref buffer, (int)bytesRead);
|
|
}
|
|
|
|
return buffer;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false)
|
|
{
|
|
if (isRelative)
|
|
address = GetAbsolute(address);
|
|
|
|
if (!NativeMethods.WriteProcessMemory(_handle, address, bytes, bytes.Length, out nint written))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return (int)written;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Dispose()
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
_disposed = true;
|
|
_handle.Dispose();
|
|
}
|
|
}
|
|
}
|