From c40f3fd7916d3f351cbec32abad1dabd67bfc901 Mon Sep 17 00:00:00 2001 From: Kevin Bataille Date: Wed, 22 Jul 2026 01:28:42 +0200 Subject: [PATCH] Fix InputSimulator wParam, PeHeaderParser double-parse, and other review issues Bug fixes: - InputSimulator: Pass correct button state (MK_LBUTTON/MK_RBUTTON) in wParam for button-down messages instead of 0. - PeHeaderParser: ParseOptionalHeader now reads only the optional header, not section headers (fixes double-parse waste). - EntryPoint: Removed useless isPe32Plus branch (AddressOfEntryPoint is at offset 16 in both PE32 and PE32+). - RemoteWindow: Handle null foreground window case in Activate to avoid calling GetWindowThreadProcessId with HWND 0. - RemotePointer: Remove dead null-conditional operators (encoding ??) since encoding is non-nullable. Constants added: - SystemMethods: MkLButton (0x0001) and MkRButton (0x0002) for mouse button state flags. Tests: 199 passing, 4 integration/interactive skipped. --- WhiteMagic/Discovery/PeHeaderParser.cs | 58 +++++++++++++++++++------- WhiteMagic/Input/InputSimulator.cs | 9 ++-- WhiteMagic/Native/SystemMethods.cs | 4 ++ WhiteMagic/RemotePointer.cs | 5 ++- WhiteMagic/Windows/RemoteWindow.cs | 7 +++- 5 files changed, 60 insertions(+), 23 deletions(-) diff --git a/WhiteMagic/Discovery/PeHeaderParser.cs b/WhiteMagic/Discovery/PeHeaderParser.cs index 723ba70..bb24d2d 100644 --- a/WhiteMagic/Discovery/PeHeaderParser.cs +++ b/WhiteMagic/Discovery/PeHeaderParser.cs @@ -64,21 +64,8 @@ public sealed class PeHeaderParser if (optionalHeader is null) return IntPtr.Zero; - // Entry point is at different offsets for PE32 vs PE32+ - bool isPe32Plus = IsPe32Plus(); - - if (isPe32Plus) - { - // PE32+: AddressOfEntryPoint is at offset 16 in OPTIONAL_HEADER (64-bit) - return (IntPtr)BitConverter.ToUInt32( - optionalHeader.AsSpan(16, 4)); - } - else - { - // PE32: AddressOfEntryPoint is at offset 16 in OPTIONAL_HEADER (32-bit) - return (IntPtr)BitConverter.ToUInt32( - optionalHeader.AsSpan(16, 4)); - } + // Entry point RVA is at offset 16 in the optional header (both PE32 and PE32+) + return (IntPtr)BitConverter.ToUInt32(optionalHeader.AsSpan(16, 4)); } } @@ -185,7 +172,46 @@ public sealed class PeHeaderParser /// private (byte[]? OptionalHeader, byte[][]? SectionHeaders) ParseOptionalHeader() { - return ParseOptionalHeaderAndSectionHeaders(); + // Read DOS header (first 64 bytes) + byte[] dosHeader = _memory.ReadBytes(_baseAddress, 64); + if (dosHeader.Length < 64) + throw new InvalidDataException("Failed to read DOS header."); + + // Verify DOS signature "MZ" + if (dosHeader[0] != 0x4D || dosHeader[1] != 0x5A) + throw new InvalidDataException("Invalid DOS signature (not a PE file)."); + + // PE header offset is at 0x3C in DOS header + int peOffset = BitConverter.ToInt32(dosHeader, 0x3C); + if (peOffset < 0 || peOffset > 0x1000) // Sanity check + throw new InvalidDataException($"Invalid PE offset: {peOffset}"); + + // Read PE signature (4 bytes: "PE\0\0") + IntPtr peSigAddr = _baseAddress + peOffset; + byte[] peSignature = _memory.ReadBytes(peSigAddr, 4); + if (peSignature.Length < 4) + throw new InvalidDataException("Failed to read PE signature."); + + if (peSignature[0] != 0x50 || peSignature[1] != 0x45 || + peSignature[2] != 0x00 || peSignature[3] != 0x00) + throw new InvalidDataException("Invalid PE signature."); + + // COFF header follows PE signature (20 bytes) + IntPtr coffAddr = peSigAddr + 4; + byte[] coffHeader = _memory.ReadBytes(coffAddr, 20); + if (coffHeader.Length < 20) + throw new InvalidDataException("Failed to read COFF header."); + + // SizeOfOptionalHeader is at offset 16 in COFF header + ushort sizeOfOptionalHeader = BitConverter.ToUInt16(coffHeader, 16); + + // Optional header follows COFF header + IntPtr optAddr = coffAddr + 20; + byte[] optionalHeader = _memory.ReadBytes(optAddr, sizeOfOptionalHeader); + if (optionalHeader.Length < sizeOfOptionalHeader) + throw new InvalidDataException("Failed to read optional header."); + + return (optionalHeader, null); } /// diff --git a/WhiteMagic/Input/InputSimulator.cs b/WhiteMagic/Input/InputSimulator.cs index 667bf6d..197513c 100644 --- a/WhiteMagic/Input/InputSimulator.cs +++ b/WhiteMagic/Input/InputSimulator.cs @@ -53,16 +53,17 @@ public sealed class InputSimulator nint lParam = MakeLong(x, y); - (uint down, uint up) = button switch + (uint down, uint downWParam) = button switch { - MouseButton.Left => (NativeMethods.WmLButtonDown, NativeMethods.WmLButtonUp), - MouseButton.Right => (NativeMethods.WmRButtonDown, NativeMethods.WmRButtonUp), + MouseButton.Left => (NativeMethods.WmLButtonDown, NativeMethods.MkLButton), + MouseButton.Right => (NativeMethods.WmRButtonDown, NativeMethods.MkRButton), _ => throw new ArgumentOutOfRangeException(nameof(button)), }; - if (!NativeMethods.PostMessageW(hWnd, down, 0, lParam)) + if (!NativeMethods.PostMessageW(hWnd, down, downWParam, lParam)) return false; + uint up = button == MouseButton.Left ? NativeMethods.WmLButtonUp : NativeMethods.WmRButtonUp; return NativeMethods.PostMessageW(hWnd, up, 0, lParam); } diff --git a/WhiteMagic/Native/SystemMethods.cs b/WhiteMagic/Native/SystemMethods.cs index aec66d0..77637e2 100644 --- a/WhiteMagic/Native/SystemMethods.cs +++ b/WhiteMagic/Native/SystemMethods.cs @@ -114,6 +114,10 @@ internal static partial class NativeMethods internal const uint WmRButtonDown = 0x0204; internal const uint WmRButtonUp = 0x0205; + // Mouse button state flags for WM_*BUTTONDOWN messages + internal const uint MkLButton = 0x0001; + internal const uint MkRButton = 0x0002; + internal static readonly IntPtr HwndTop = IntPtr.Zero; internal const uint SwpShowWindow = 0x0040; diff --git a/WhiteMagic/RemotePointer.cs b/WhiteMagic/RemotePointer.cs index fae0bf8..1d5301a 100644 --- a/WhiteMagic/RemotePointer.cs +++ b/WhiteMagic/RemotePointer.cs @@ -35,15 +35,16 @@ public sealed class RemotePointer /// Reads a null-terminated string at BaseAddress + offset. public string ReadString(Encoding encoding, int maxLength = 512, nint offset = 0) { - return _memory.ReadString(BaseAddress + offset, encoding ?? Encoding.UTF8, maxLength); + return _memory.ReadString(BaseAddress + offset, encoding, maxLength); } /// Writes a null-terminated string at BaseAddress + offset. public bool WriteString(string value, Encoding encoding, nint offset = 0) { - return _memory.WriteString(BaseAddress + offset, value, encoding ?? Encoding.UTF8); + return _memory.WriteString(BaseAddress + offset, value, encoding); } + /// Returns a new with the offset added. public RemotePointer this[nint offset] => new RemotePointer(_memory, BaseAddress + offset); } diff --git a/WhiteMagic/Windows/RemoteWindow.cs b/WhiteMagic/Windows/RemoteWindow.cs index 7d8aad7..fbfead4 100644 --- a/WhiteMagic/Windows/RemoteWindow.cs +++ b/WhiteMagic/Windows/RemoteWindow.cs @@ -72,11 +72,16 @@ public sealed class RemoteWindow { IntPtr foreground = NativeMethods.GetForegroundWindow(); uint targetThread = NativeMethods.GetWindowThreadProcessId(Handle, out _); - uint foregroundThread = NativeMethods.GetWindowThreadProcessId(foreground, out _); if (targetThread == 0) return false; + // If there's no foreground window, or we're already in the foreground thread, just set it + if (foreground == IntPtr.Zero) + return NativeMethods.SetForegroundWindow(Handle); + + uint foregroundThread = NativeMethods.GetWindowThreadProcessId(foreground, out _); + if (targetThread == foregroundThread) return NativeMethods.SetForegroundWindow(Handle);