From a34389fbba4fc194ea55f3cfed5d423a0b85c9f0 Mon Sep 17 00:00:00 2001 From: Kevin Bataille Date: Wed, 22 Jul 2026 02:24:06 +0200 Subject: [PATCH] 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. --- WhiteMagic/ExternalReader.cs | 14 ++++++++++--- WhiteMagic/Injection/DllInjector.cs | 29 +++++++++++++++++++------- WhiteMagic/Native/NativeMethods.cs | 14 +++++++++++++ WhiteMagicTest/MemoryHardeningTests.cs | 9 ++++++++ 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/WhiteMagic/ExternalReader.cs b/WhiteMagic/ExternalReader.cs index 74c3785..afc591f 100644 --- a/WhiteMagic/ExternalReader.cs +++ b/WhiteMagic/ExternalReader.cs @@ -37,6 +37,13 @@ public sealed class ExternalReader : MemoryBase public ExternalReader(System.Diagnostics.Process process, ProcessAccess desiredAccess = DefaultAccess) { _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); 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. - // A 32-bit host can only open 32-bit targets. If the API fails, fall - // back to the current process bitness (self-open path). + // A 32-bit host can only open 32-bit targets. 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; diff --git a/WhiteMagic/Injection/DllInjector.cs b/WhiteMagic/Injection/DllInjector.cs index 2d9f199..6458b8e 100644 --- a/WhiteMagic/Injection/DllInjector.cs +++ b/WhiteMagic/Injection/DllInjector.cs @@ -215,7 +215,7 @@ public sealed class DllInjector { IntPtr result; - if (_currentIs64Bit) + if (Environment.Is64BitProcess) { var originalContext = new Context64 { ContextFlags = ContextFlags.Amd64Full }; if (!NativeMethods.GetThreadContext(thread, ref originalContext)) @@ -264,21 +264,32 @@ public sealed class DllInjector } 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 }; - 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(); - throw new InvalidOperationException($"Wow64GetThreadContext failed (error {error})."); + string api = useWow64 ? "Wow64GetThreadContext" : "GetThreadContext"; + throw new InvalidOperationException($"{api} failed (error {error})."); } var redirectContext = originalContext; redirectContext.Eip = (uint)(nint)remoteBase; 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(); - throw new InvalidOperationException($"Wow64SetThreadContext failed (error {error})."); + string api = useWow64 ? "Wow64SetThreadContext" : "SetThreadContext"; + throw new InvalidOperationException($"{api} failed (error {error})."); } if (NativeMethods.ResumeThread(thread) == 0xFFFFFFFF) @@ -295,10 +306,14 @@ public sealed class DllInjector 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(); - 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) diff --git a/WhiteMagic/Native/NativeMethods.cs b/WhiteMagic/Native/NativeMethods.cs index 5160985..8dbca1e 100644 --- a/WhiteMagic/Native/NativeMethods.cs +++ b/WhiteMagic/Native/NativeMethods.cs @@ -138,6 +138,20 @@ internal static partial class NativeMethods SafeMemoryHandle thread, ref Context64 context); + /// Sets a 32-bit thread context (x86 or WOW64). + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetThreadContext( + SafeMemoryHandle thread, + ref Context32 context); + + /// Gets a 32-bit thread context (x86 or WOW64). + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool GetThreadContext( + SafeMemoryHandle thread, + ref Context32 context); + /// Sets a 32-bit (WOW64) thread context. [LibraryImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] diff --git a/WhiteMagicTest/MemoryHardeningTests.cs b/WhiteMagicTest/MemoryHardeningTests.cs index e8647a7..d8c5958 100644 --- a/WhiteMagicTest/MemoryHardeningTests.cs +++ b/WhiteMagicTest/MemoryHardeningTests.cs @@ -151,6 +151,15 @@ public class MemoryHardeningTests Assert.False(reader.Handle.IsInvalid); } + [Fact] + public void ExternalReader_throws_when_query_information_access_missing() + { + var ex = Assert.Throws(() => + new ExternalReader(Process.GetCurrentProcess(), ProcessAccess.VmRead)); + + Assert.Equal("desiredAccess", ex.ParamName); + } + /// /// A that serves bytes from an in-memory buffer and /// caps every read to maxChunk bytes, to exercise partial-read handling.