Files
whitemagic/WhiteMagic/ExternalReader.cs
T
kbeandClaude Opus 4.8 7c5e72e0e0 Harden memory layer: fix char sizing, ref structs, count guards, ReadString advance
Second-review fixes, each covered by a regression test in MemoryHardeningTests:

- MarshalCache: special-case char (Size=2; Marshal.SizeOf reports 1/ANSI but the
  blittable path reads a 2-byte UTF-16 unit). TypeRequiresMarshal now also trips on
  RuntimeHelpers.IsReferenceOrContainsReferences<T>() so reference-carrying structs
  route to the marshal path instead of throwing in MemoryMarshal.Read. Document that
  the MarshalAs scan is top-level only.
- MemoryBase.Read<T>(count): reject negative count (ArgumentOutOfRangeException) and
  guard elementSize*count overflow. Same overflow guard on Write<T>(values).
- MemoryBase.ReadString: advance by bytes actually read, not the requested amount, so
  a partial read no longer skips the unread tail of the window.
- ExternalReader: default to a minimal access set (not AllAccess, which over-requests
  and fails on protected processes); wrap Process.MainModule in try/catch so a
  bitness-mismatched or protected target yields ImageBase=Zero instead of throwing.
- NativeMethods: WaitForSingleObject and CreateRemoteThread's threadId are DWORD (uint),
  not int — the signatures no longer sign-flip.

Deferred: hoisting the identical ExternalReader/InProcessReader byte-IO into MemoryBase
(cosmetic; skipped to avoid colliding with concurrent Phase 3 edits).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 19:47:24 +02:00

106 lines
3.3 KiB
C#

using System.Diagnostics;
using System.Runtime.InteropServices;
using WhiteMagic.Native;
namespace WhiteMagic;
/// <summary>
/// Out-of-process memory reader that accesses the target's memory through
/// <see cref="NativeMethods.ReadProcessMemory"/> and
/// <see cref="NativeMethods.WriteProcessMemory"/>.
/// </summary>
public sealed class ExternalReader : MemoryBase
{
private readonly SafeMemoryHandle _handle;
private readonly IntPtr _imageBase;
private bool _disposed;
/// <summary>
/// The default access rights: enough to read, write, allocate, query, run a remote
/// thread, and wait on it. This deliberately omits <see cref="ProcessAccess.AllAccess"/>,
/// which over-requests and makes <c>OpenProcess</c> fail on protected processes where
/// these narrower rights would succeed.
/// </summary>
public const ProcessAccess DefaultAccess =
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation
| ProcessAccess.QueryInformation | ProcessAccess.CreateThread | ProcessAccess.Synchronize;
/// <summary>
/// Opens a process for external memory access.
/// </summary>
/// <param name="process">The target process.</param>
/// <param name="desiredAccess">The access rights to request. Defaults to
/// <see cref="DefaultAccess"/>.</param>
public ExternalReader(Process process, ProcessAccess desiredAccess = DefaultAccess)
{
_handle = NativeMethods.OpenProcess(desiredAccess, false, process.Id);
if (_handle.IsInvalid)
{
int error = Marshal.GetLastPInvokeError();
throw new InvalidOperationException(
$"OpenProcess failed for PID {process.Id}: error {error}");
}
// Process.MainModule throws Win32Exception for a bitness-mismatched or protected
// target; a missing image base must not sink the whole reader.
try
{
_imageBase = process.MainModule?.BaseAddress ?? IntPtr.Zero;
}
catch (System.ComponentModel.Win32Exception)
{
_imageBase = 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();
}
}
}