Fix 32-bit host context APIs and ExternalReader bitness detection
- Add GetThreadContext/SetThreadContext overloads accepting Context32 so a 32-bit process on a native 32-bit OS can capture x86 thread context. - DllInjector.InjectWithThreadHijack now selects the context API based on both process bitness and OS bitness: * 64-bit process -> native 64-bit context * 32-bit process on 64-bit OS -> WOW64 context * 32-bit process on 32-bit OS -> native x86 context - ExternalReader now validates that the caller supplied ProcessAccess.QueryInformation, and surfaces any IsWow64Process failure instead of silently falling back to host bitness. Tests: 207 passing, 4 skipped.
This commit is contained in:
@@ -37,6 +37,13 @@ public sealed class ExternalReader : MemoryBase
|
|||||||
public ExternalReader(System.Diagnostics.Process process, ProcessAccess desiredAccess = DefaultAccess)
|
public ExternalReader(System.Diagnostics.Process process, ProcessAccess desiredAccess = DefaultAccess)
|
||||||
{
|
{
|
||||||
_processId = process.Id;
|
_processId = process.Id;
|
||||||
|
if ((desiredAccess & ProcessAccess.QueryInformation) == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"ExternalReader requires ProcessAccess.QueryInformation to determine target bitness.",
|
||||||
|
nameof(desiredAccess));
|
||||||
|
}
|
||||||
|
|
||||||
_handle = NativeMethods.OpenProcess(desiredAccess, false, _processId);
|
_handle = NativeMethods.OpenProcess(desiredAccess, false, _processId);
|
||||||
if (_handle.IsInvalid)
|
if (_handle.IsInvalid)
|
||||||
{
|
{
|
||||||
@@ -46,11 +53,12 @@ public sealed class ExternalReader : MemoryBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Derive target bitness. A 64-bit host sees a 32-bit target as WOW64.
|
// Derive target bitness. A 64-bit host sees a 32-bit target as WOW64.
|
||||||
// A 32-bit host can only open 32-bit targets. If the API fails, fall
|
// A 32-bit host can only open 32-bit targets.
|
||||||
// back to the current process bitness (self-open path).
|
|
||||||
if (!NativeMethods.IsWow64Process(_handle, out bool wow64))
|
if (!NativeMethods.IsWow64Process(_handle, out bool wow64))
|
||||||
{
|
{
|
||||||
wow64 = false;
|
int error = Marshal.GetLastPInvokeError();
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"IsWow64Process failed for PID {_processId}: error {error}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_is64Bit = Environment.Is64BitProcess && !wow64;
|
_is64Bit = Environment.Is64BitProcess && !wow64;
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ public sealed class DllInjector
|
|||||||
{
|
{
|
||||||
IntPtr result;
|
IntPtr result;
|
||||||
|
|
||||||
if (_currentIs64Bit)
|
if (Environment.Is64BitProcess)
|
||||||
{
|
{
|
||||||
var originalContext = new Context64 { ContextFlags = ContextFlags.Amd64Full };
|
var originalContext = new Context64 { ContextFlags = ContextFlags.Amd64Full };
|
||||||
if (!NativeMethods.GetThreadContext(thread, ref originalContext))
|
if (!NativeMethods.GetThreadContext(thread, ref originalContext))
|
||||||
@@ -264,21 +264,32 @@ public sealed class DllInjector
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// 32-bit process on either a 32-bit or 64-bit (WOW64) host.
|
||||||
|
bool useWow64 = Environment.Is64BitOperatingSystem;
|
||||||
|
|
||||||
var originalContext = new Context32 { ContextFlags = ContextFlags.X86Full };
|
var originalContext = new Context32 { ContextFlags = ContextFlags.X86Full };
|
||||||
if (!NativeMethods.Wow64GetThreadContext(thread, ref originalContext))
|
bool gotContext = useWow64
|
||||||
|
? NativeMethods.Wow64GetThreadContext(thread, ref originalContext)
|
||||||
|
: NativeMethods.GetThreadContext(thread, ref originalContext);
|
||||||
|
if (!gotContext)
|
||||||
{
|
{
|
||||||
int error = Marshal.GetLastPInvokeError();
|
int error = Marshal.GetLastPInvokeError();
|
||||||
throw new InvalidOperationException($"Wow64GetThreadContext failed (error {error}).");
|
string api = useWow64 ? "Wow64GetThreadContext" : "GetThreadContext";
|
||||||
|
throw new InvalidOperationException($"{api} failed (error {error}).");
|
||||||
}
|
}
|
||||||
|
|
||||||
var redirectContext = originalContext;
|
var redirectContext = originalContext;
|
||||||
redirectContext.Eip = (uint)(nint)remoteBase;
|
redirectContext.Eip = (uint)(nint)remoteBase;
|
||||||
redirectContext.Esp = (uint)(nint)stackTop;
|
redirectContext.Esp = (uint)(nint)stackTop;
|
||||||
|
|
||||||
if (!NativeMethods.Wow64SetThreadContext(thread, ref redirectContext))
|
bool setContext = useWow64
|
||||||
|
? NativeMethods.Wow64SetThreadContext(thread, ref redirectContext)
|
||||||
|
: NativeMethods.SetThreadContext(thread, ref redirectContext);
|
||||||
|
if (!setContext)
|
||||||
{
|
{
|
||||||
int error = Marshal.GetLastPInvokeError();
|
int error = Marshal.GetLastPInvokeError();
|
||||||
throw new InvalidOperationException($"Wow64SetThreadContext failed (error {error}).");
|
string api = useWow64 ? "Wow64SetThreadContext" : "SetThreadContext";
|
||||||
|
throw new InvalidOperationException($"{api} failed (error {error}).");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NativeMethods.ResumeThread(thread) == 0xFFFFFFFF)
|
if (NativeMethods.ResumeThread(thread) == 0xFFFFFFFF)
|
||||||
@@ -295,10 +306,14 @@ public sealed class DllInjector
|
|||||||
throw new InvalidOperationException($"SuspendThread failed while capturing result (error {error}).");
|
throw new InvalidOperationException($"SuspendThread failed while capturing result (error {error}).");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!NativeMethods.Wow64SetThreadContext(thread, ref originalContext))
|
bool restoredContext = useWow64
|
||||||
|
? NativeMethods.Wow64SetThreadContext(thread, ref originalContext)
|
||||||
|
: NativeMethods.SetThreadContext(thread, ref originalContext);
|
||||||
|
if (!restoredContext)
|
||||||
{
|
{
|
||||||
int error = Marshal.GetLastPInvokeError();
|
int error = Marshal.GetLastPInvokeError();
|
||||||
throw new InvalidOperationException($"Wow64SetThreadContext restore failed (error {error}).");
|
string api = useWow64 ? "Wow64SetThreadContext" : "SetThreadContext";
|
||||||
|
throw new InvalidOperationException($"{api} restore failed (error {error}).");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NativeMethods.ResumeThread(thread) == 0xFFFFFFFF)
|
if (NativeMethods.ResumeThread(thread) == 0xFFFFFFFF)
|
||||||
|
|||||||
@@ -138,6 +138,20 @@ internal static partial class NativeMethods
|
|||||||
SafeMemoryHandle thread,
|
SafeMemoryHandle thread,
|
||||||
ref Context64 context);
|
ref Context64 context);
|
||||||
|
|
||||||
|
/// <summary>Sets a 32-bit thread context (x86 or WOW64).</summary>
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SetThreadContext(
|
||||||
|
SafeMemoryHandle thread,
|
||||||
|
ref Context32 context);
|
||||||
|
|
||||||
|
/// <summary>Gets a 32-bit thread context (x86 or WOW64).</summary>
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool GetThreadContext(
|
||||||
|
SafeMemoryHandle thread,
|
||||||
|
ref Context32 context);
|
||||||
|
|
||||||
/// <summary>Sets a 32-bit (WOW64) thread context.</summary>
|
/// <summary>Sets a 32-bit (WOW64) thread context.</summary>
|
||||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
|||||||
@@ -151,6 +151,15 @@ public class MemoryHardeningTests
|
|||||||
Assert.False(reader.Handle.IsInvalid);
|
Assert.False(reader.Handle.IsInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExternalReader_throws_when_query_information_access_missing()
|
||||||
|
{
|
||||||
|
var ex = Assert.Throws<ArgumentException>(() =>
|
||||||
|
new ExternalReader(Process.GetCurrentProcess(), ProcessAccess.VmRead));
|
||||||
|
|
||||||
|
Assert.Equal("desiredAccess", ex.ParamName);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A <see cref="MemoryBase"/> that serves bytes from an in-memory buffer and
|
/// A <see cref="MemoryBase"/> that serves bytes from an in-memory buffer and
|
||||||
/// caps every read to <c>maxChunk</c> bytes, to exercise partial-read handling.
|
/// caps every read to <c>maxChunk</c> bytes, to exercise partial-read handling.
|
||||||
|
|||||||
Reference in New Issue
Block a user