Hello, and Happy New Year! This article is the second in a series that looks at the interception of window messages with managed code and shall be my first article in this (hopefully glorious) new year of 2024.
Whereas the first article focused on intercepting messages from windows belonging to the same process hosting our .NET code via subclassing, this article will look at a much spicier topic: viewing messages being sent to external, out-of-process windows using Win32 hooks.
This topic is extra spicy because, as you may or may not know, (the vast majority of) Win32 hooks are not supported in .NET. And by not supported, I mean they do not work. And for good reason.
So how do we get them to work and spy on those juicy external window messages? Well, read on…
Receiving Out-Of-Process Window Messages
When thinking of intercepting messages sent to arbitrary windows, the first thing that comes to mind is the Spy++ tool that gets bundled with Visual Studio.
So, when asking myself how to intercept external window messages, what better teacher to learn from than this fabled debugging tool? I donned my research hat and did some Googling until I came across an older article written by a VC++ design engineer who worked on Spy++ that answered this very question.
As the article explains, it intercepts messages through the use of three Win32 hooks, namely:
WH_CALLWNDPROC
(called prior to a window procedure executing)WH_CALLWNDPROCRET
(called after a window procedure returns)WH_GETMESSAGE
(called when a message is about to be returned from a message queue)
WH_CALLWNDPROC
is all we need to leverage to intercept external window messages, but we will also be spending some time looking at WH_GETMESSAGE
for a bit of extra credit.
But, What Are Win32 Hooks Anyway?
Microsoft documentation refers to them simply as hooks; however, I find that much too general of a designation, so I find myself having to add the additional qualifier of “Win32”.
Win32 hooks are a means to be notified of potentially system-wide events. You can find general documentation on their use on Microsoft’s website.
The use of Win32 hooks is an advanced Windows OS programming topic, where little mistakes can bring your whole system down to its knees!
That isn’t what we should be worried about, however. Rather, we should be more concerned with the fact that we’re writing managed code, something that doesn’t get along nicely with Win32 hooks.
Why Win32 Hooks Don’t Work With .NET
External hook procedures are installed via the injection of DLLs containing said procedures into a target process or processes. There are a few issues when trying to do this with .NET:
- Managed DLL assemblies cannot export functions like native DLLs can.
- Processes running only unmanaged code and lacking a loaded .NET runtime cannot execute managed code.
There’s really no way around it: our hook procedure for capturing external window messages cannot be written in managed code.
A Deeper Look At Hook DLL Injection
Hook procedures are installed using the SetWindowsHookEx
Win32 function and can be installed either to a specific thread or system-wide (resulting in a global hook).
If either a thread not belonging to the current process (*cough* such as a thread an external window is running on) or a global hook is intended, then the hMod
parameter of SetWindowsHookEx
must be set to a handle to the DLL containing the hook procedure pointed to by the lpfn
parameter.
The DLL pointed to by hMod
is injected into the target process or processes, with the expectation that the hook procedure is a DLL export that can act as a valid, consistent function to call into.
.NET does not support DLL exports: managed DLL assemblies lack the necessary facilities that allow arbitrary processes to execute code via proffered “function pointers”. Managed code has no concept of consistent values for function pointers because marshalled function pointers are merely dynamically built proxies.
Even if .NET DLLs did have such facilities, there is as good a chance as any that our targeted processes are unmanaged and do not have a loaded .NET runtime.
Some Hooks Do Work With .NET
Hooks that always run on the same thread as the code that installed them actually do work with .NET. That’s because there is no DLL injection required. Two examples of hooks like this are the low-level keyboard and mouse hooks.
These kinds of hooks are unique in the sense that despite being global, they are run on the thread that installed them as opposed to the thread processing the hook.
A Native Hooks Library We Can Inject
If you thought you would get to spy on those juicy external window messages without having to step out of the safety of Land of the Managed Code, you were dead wrong my friend.
We will need to write a native DLL, and we’ll be doing it using good ol’ C++ (though the code we write will be a bit more C-like as we’re not going to deal with creating any classes or the like). We’ll add the things we need it to do in order for it to intercept external window messages, and we’ll also take a look at how we can add support for other hook types.
But, to start things off just what does it need to do?
Native Hooks Library Requirements
- Exports functions that will add/remove Win32 hooks
- Has some type of IPC allowing it to share data with other (injected) instances of itself
- Has some kind of mechanism allowing for the native DLL to send message data back to a managed caller
- Has the means for a managed caller to modify hook messages that are mutable (this depends on the hook type)
Lets examine each of the requirements and how we’ll fulfill them.
Function Exports For Adding/Removing Win32 Hooks
We’ll have one function for installing the hook procedure, and one for uninstalling them.
Hooks.h – Adding/Removing Hooks
#define HOOKS_API extern "C" __declspec(dllexport) /** * Installs a new Win32 hook procedure into the specified thread. * @param hookType The type of hook procedure to install. * @param threadId The identifier of the thread with which the hook procedure is to be associated. * @param destination A handle to the window that will receive messages sent to the hook procedure. * @return True if successful; otherwise, false. */ HOOKS_API bool __cdecl AddHook(HookType hookType, int threadId, HWND destination); /** * Uninstalls a Win32 hook procedure from the specified thread. * @param hookType The type of hook procedure to uninstall. * @param threadId The identifier of the thread to remove the hook procedure from. * @return True if successful; otherwise, false. */ HOOKS_API bool __cdecl RemoveHook(HookType hookType, int threadId);
There are two exported functions for installing and uninstalling hook procedures shown above. The specific hook type installed/uninstalled when invoking them depends on the HookType
enum value provided.
As one can judge by the signatures, these functions are for hooks targeting specific threads/processes, as evidenced by the threadId. We’ll add some additional functions
Hooks.h – HookType Enum
/** * Specifies a type of hook procedure. */ enum HookType { /** * Monitors \c WH_CALLWNDPROC messages before the system sends them to a destination window * procedure. */ CallWindowProcedure, /** * Monitors \c WH_CALLWNDPROCRET messages after they have been processed by the destination * window procedure. */ CallWindowProcedureReturn, /** * Monitors \c WH_GETMESSAGE messages posted to a message queue prior to their retrieval. */ GetMessage };
We only have three hook types specified in the above enum definition; however, this is all we’ll need for this article. CallWindowProcedure
will be used to intercept external window messages, and we’ll also look at GetMessage
and all the interesting things that emerge with its use.
An individual callback for each hook procedure type is used.
Hooks.h – Hook Procedures
// Installable hook procedures. LRESULT CALLBACK CallWndProc(int nCode, WPARAM wParam, LPARAM lParam); LRESULT CALLBACK CallWndProcRet(int nCode, WPARAM wParam, LPARAM lParam); LRESULT CALLBACK GetMsgProc(int code, WPARAM wParam, LPARAM lParam);
That’s not a typo in the first parameter for GetMsgProc
…for whatever reason, Microsoft kept flip-flopping between using code
and nCode
for their various hook procedures (I like to be at parity with Microsoft’s docs, for the most part).
We’ll go into implementations for all the above declarations later in the article; for now, let’s look at some of the other requirements our native Hooks DLL must fulfill.
Sharing Memory Between DLL Instances
Our managed code will call hook registration functions exported from our native Hooks library via P/Invoke. These registration functions will call SetWindowsHookEx
and provide a handle to the DLL that contains them for the hMod
parameter, causing the Hooks DLL to be injected into the targeted process.
These registration functions will also accept some configuration information, such as where or how to relay messages received by the hook procedure. However, the instance of our DLL processing the hook procedure will be in a separate process and share no memory with the instance we have configured via P/Invoke.
So, how do we get the configuration information over to the injected DLL instances? By sharing memory, of course! Memory shall be shared by means of memory-mapped files that the system paging file stores.
This is done by calling CreateFileMapping
, passing INVALID_HANDLE_VALUE
so the paging file is used, and then MapViewOfFile
to associate the shared data with the virtual address space of the process. This will be done while inside the DllMain
method.
DllMain.cpp – Shared Memory Initialization
BOOL APIENTRY DllMain(HINSTANCE instance, DWORD reason, LPVOID) { BOOL init; switch (reason) { case DLL_PROCESS_ATTACH: Instance = instance; FileMapping = CreateFileMapping( INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, SharedMemorySize, TEXT("BadEcho.Hooks.FileMappingObject")); if (FileMapping == nullptr) return FALSE; init = GetLastError() != ERROR_ALREADY_EXISTS; SharedMemory = MapViewOfFile(FileMapping, FILE_MAP_WRITE, 0, 0, 0); if (SharedMemory == nullptr) return FALSE; SharedSectionMutex = CreateMutex(nullptr, FALSE, TEXT("BadEcho.Hooks.MutexObject")); if (SharedSectionMutex == nullptr) return FALSE; if (init) memset(SharedMemory, '\0', SharedMemorySize); SharedData = static_cast<ThreadData*>(SharedMemory); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: UnmapViewOfFile(SharedMemory); CloseHandle(FileMapping); CloseHandle(SharedSectionMutex); break; default: return FALSE; } return TRUE; }
The above code will load the shared memory for every instance of the DLL being injected into a process, initializing the memory during the first DllMain
invocation (typically from a P/Invoke call).
You’ll notice that the data stored in the shared memory is a pointer to a bunch of ThreadData
objects. Let’s take a look at that.
Hook.h – ThreadData and HookData
/** * Represents configurations settings for a hook procedure. */ struct HookData { /** * A handle to the hook procedure. */ HHOOK Handle; /** * A handle to the window that hook messages will be sent to. */ HWND Destination; }; /** * Represents shared hook data specific to a thread. */ struct ThreadData { /** * The thread the data is associated with. */ int ThreadId; /** * The installed \c WH_CALLWNDPROC hook procedure for the thread, if one exists. */ HookData CallWndProcHook; /** * The installed \c WH_CALLWNDPROCRET hook procedure for the thread, if one exists. */ HookData CallWndProcRetHook; /** * The installed \c WH_GETMESSAGE hook procedure for the thread, if one exists. */ HookData GetMessageHook; };
Each thread targeted by one or more installed hooks gets allocated an associated ThreadData
value. Each value contains the thread’s ID and the various HookData
values for the hooks that we’ve registered.
HookData
values contain the handle to the hook and where to send the message when our hook procedure is processing it — a topic we’ll cover now.
Sending Messages Back to Managed Code
One of the biggest seeming challenges here is the matter of relaying the intercepted messages from the process that is receiving them back to the one running our managed code. We need a way for unmanaged code running in one process to notify a managed component running in another.
Not the simplest of problems to solve. One potential solution would be to have our managed code run as a managed out-of-proc COM server; however, that approach is, for lack of better words a bit much…
Perhaps that’s something that can be explored later; until then, a much simpler solution would be to actually reroute the messages via a call to SendMessage
, with the target HWND
being a message-only window running in our managed process.
This target HWND
is what the Destination
member of the relevant HookData
will be set to, and it is provided as a parameter when calling the AddHook
registration function.
We’ll take a closer look at how we receive the messages on the managed side of things during the .NET counterpart of this article.
Modifying Mutable Hook Messages From Managed Code
Some hook types have message parameters that can be modified by the hook procedure, potentially affecting the behavior of the process we’ve injected into. If we wish to engage in this practice, we must devise a way to propagate the desired changes from our managed code following a message notification back to the appropriate DLL instance.
The first thing we’ll need is another exported function that we can call from our managed code to modify the hook message’s details.
Hooks.h – Changing Message Details
/** * Changes the details of a hook message currently being intercepted. * @param message The message identifier to use. * @param wParam Additional information about the message to use. * @param lParam Additional information about the message to use. * @note * This function should only be called from window procedures that handle hook types supporting * mutable messages. */ HOOKS_API void __cdecl ChangeMessageDetails(UINT message, WPARAM wParam, LPARAM lParam);
The above function can be P/Invoked while a managed window procedure set as a destination for a hook type supporting mutable messages is executing. The supplied parameter values will overwrite the values found in the message details struct passed in by the hook procedure.
However, the DLL instance receiving this function call will be the one loaded into the managed caller’s process. How will it share these updated values with the DLL instance processing the hook message?
There are a number of ways, but let us mix it up a bit and use DLL shared data segments.
Hooks.h – Shared Data Segments
// Add a data section to our binary file for variables we want shared across all injected // processes. // The variables that are shared mainly deal with the number of active hooks and message // parameters up for modification. #pragma data_seg(".shared") inline bool ChangeMessage = false; inline UINT ChangedMessage = 0; inline WPARAM ChangedWParam = 0; inline LPARAM ChangedLParam = 0; inline int ThreadCount = 0; #pragma data_seg() #pragma comment(linker, "/SECTION:.shared,RWS")
The PE format Windows uses for its EXE and DLL files defines the notion of data sections. The above code creates a new data section that will appear in our binary file and then instructs the linker that the just-created section should be marked as shared.
By default, each process using a DLL has its own instance of all the DLL’s global and static variables. However, variables declared inside a shared data segment will be shared across all processes using the DLL.
Note that the DLL instances must all originate from the exact same physical file for the shared data segment members to be shared. This will be the case for us, so we needn’t worry.
Using a memory-mapped file like we do with our ThreadData
would also work here, but again, I wanted to mix it up a bit (not to mention that this approach simplifies things).
However, we will need to provide synchronization in the form of a mutex to support having the particular hook procedure installed in multiple processes.
Function Definitions
With the groundwork laid out, let’s take a look at the actual body of code for the various functions mentioned above.
DllMain.cpp – Function Definitions
ThreadData *GetLocalData(int threadId, bool addEntry) { int index; for (index = 0; index < ThreadCount; index++) { if (SharedData[index].ThreadId == threadId) break; } // Thread not registered -- attempt to initialize data. if (index == ThreadCount) { if (!addEntry || ThreadCount == MaxThreads) return nullptr; SharedData[index].ThreadId = threadId; SharedData[index].CallWndProcHook.Handle = nullptr; SharedData[index].CallWndProcHook.Destination = nullptr; SharedData[index].CallWndProcRetHook.Handle = nullptr; SharedData[index].CallWndProcRetHook.Destination = nullptr; SharedData[index].GetMessageHook.Handle = nullptr; SharedData[index].GetMessageHook.Destination = nullptr; // Synchronization is required as multiple processes may be attempting to increment the // thread count. WaitForSingleObject(SharedSectionMutex, INFINITE); ThreadCount++; ReleaseMutex(SharedSectionMutex); } return &SharedData[index]; } bool __cdecl AddHook(HookType hookType, int threadId, HWND destination) { ThreadData* localData = GetLocalData(threadId, true); if (localData == nullptr) return false; HookData* hookData; int idHook; HOOKPROC lpfn; switch (hookType) { case CallWindowProcedure: hookData = &localData->CallWndProcHook; idHook = WH_CALLWNDPROC; lpfn = CallWndProc; break; case CallWindowProcedureReturn: hookData = &localData->CallWndProcRetHook; idHook = WH_CALLWNDPROCRET; lpfn = CallWndProcRet; break; case GetMessage: hookData = &localData->GetMessageHook; idHook = WH_GETMESSAGE; lpfn = GetMsgProc; break; default: return false; } HHOOK hook = SetWindowsHookEx(idHook, lpfn, Instance, threadId); if (hook == nullptr) return false; hookData->Handle = hook; hookData->Destination = destination; return true; } bool __cdecl RemoveHook(HookType hookType, int threadId) { ThreadData* localData = GetLocalData(threadId, false); if (localData == nullptr) return false; HookData* hookData; switch (hookType) { case CallWindowProcedure: hookData = &localData->CallWndProcHook; break; case CallWindowProcedureReturn: hookData = &localData->CallWndProcRetHook; break; case GetMessage: hookData = &localData->GetMessageHook; break; default: return false; } if (hookData->Handle == nullptr) return false; bool result = UnhookWindowsHookEx(hookData->Handle); hookData->Handle = nullptr; hookData->Destination = nullptr; return result; } void __cdecl ChangeMessageDetails(UINT message, WPARAM wParam, LPARAM lParam) { ChangedMessage = message; ChangedWParam = wParam; ChangedLParam = lParam; ChangeMessage = true; } LRESULT CALLBACK CallWndProc(int nCode, WPARAM wParam, LPARAM lParam) { int threadId = static_cast<int>(GetCurrentThreadId()); if (ThreadData* localData = GetLocalData(threadId, false); nCode == HC_ACTION) { HWND destination = localData->CallWndProcHook.Destination; auto messageParameters = PointTo<CWPSTRUCT>(lParam); if (destination != nullptr) { SendMessage( destination, messageParameters->message, messageParameters->wParam, messageParameters->lParam); } } return CallNextHookEx(nullptr, nCode, wParam, lParam); } LRESULT CALLBACK CallWndProcRet(int nCode, WPARAM wParam, LPARAM lParam) { int threadId = static_cast<int>(GetCurrentThreadId()); if (ThreadData* localData = GetLocalData(threadId, false); nCode == HC_ACTION) { HWND destination = localData->CallWndProcRetHook.Destination; auto messageParameters = PointTo<CWPRETSTRUCT>(lParam); if (destination != nullptr) { SendMessage( destination, messageParameters->message, messageParameters->wParam, messageParameters->lParam); } } return CallNextHookEx(nullptr, nCode, wParam, lParam); } LRESULT CALLBACK GetMsgProc(int code, WPARAM wParam, LPARAM lParam) { int threadId = static_cast<int>(GetCurrentThreadId()); if (ThreadData* localData = GetLocalData(threadId, false); code == HC_ACTION) { if (HWND destination = localData->GetMessageHook.Destination; destination != nullptr) { // Unlike some of these other hooks, we are able to modify messages of this hook type // before control is returned to the system. auto messageParameters = PointTo<MSG>(lParam); WaitForSingleObject(SharedSectionMutex, INFINITE); __try { ChangeMessage = false; SendMessage( destination, messageParameters->message, messageParameters->wParam, messageParameters->lParam); if (ChangeMessage) { messageParameters->message = ChangedMessage; messageParameters->wParam = ChangedWParam; messageParameters->lParam = ChangedLParam; } } __finally { ReleaseMutex(SharedSectionMutex); } } } return CallNextHookEx(nullptr, code, wParam, lParam); }
These functions are pretty straightforward and fulfill the expectations we laid out in the previous sections. You can find the complete, up-to-date source code for the BadEcho.Hooks library here.
Let us now leap out of the unmanaged universe and dive into the managed code counterpart to all of this C++ tomfoolery.
Wrapping an External Window and Its Messages
In the previous article of this series, we looked at classes concerned with wrapping a local (in-process) window’s handle and the messages it receives, with the star of the show being the LocalWindowWrapper
class.
To intercept messages from out-of-proc windows, we’ll create another derivation of the WindowWrapper
base class, which LocalWindowWrapper
shares as an ancestor: the GlobalWindowWrapper
.
How It Will Work
The GlobalWindowWrapper
will be similar to the LocalWindowWrapper
type in that it will take a HWND
, or IntPtr
if you will, to “wrap” as well as offering us the ability to add our custom hooks to the window procedure via the AddHook
/RemoveHook
methods.
LocalWindowWrapper
used subclassing to intercept the window’s messages; however, that option is not available for GlobalWindowWrapper
because the windows are in a different process. Instead of subclassing, we will be P/Invoking the various unmanaged function exports we just wrote.
To do that, we need to find the ID of the thread that the external window is running on (easy with the GetWindowThreadProcessId
function), but we’ll also need to provide a destination HWND
for our native DLL to use to relay an intercepted message back on.
This destination HWND
must be an independent window local to our managed code’s process. There’s a good chance that (unless we happen to be a desktop application) we don’t have a spare HWND
and an equally good chance that we don’t even want one.
We need a window that is not a window; no, not Schrödinger’s cat, but a message-only window.
Wrapping a Message-Only Window
A message-only window is one with the singular purpose of receiving window messages. It has no physical manifestation; no shape, size, look, or feel. There’s a good chance you have many, many message-only windows spawned on your desktop this very instant, deviously lurking in the shadows.
To create one from a .NET codebase, we’ll once again take advantage of our window wrapping object hierarchy we started in the previous article. Deriving from WindowWrapper
, we have a MessageOnlyWindowWrapper
type, which provides window procedure hook management for a message-only window.
Initializing a MessageOnlyWindowWrapper
will create and manage a message-only window. Details about how message-only windows work and how to make them are outside this article’s scope, so please refer to the source of MessageOnlyWindowWrapper
here.
Dead Wrappers
The MessageOnlyWindowWrapper
type will conduct all steps required to successfully create a message-only window whose window procedure points to our managed WindowWrapper.WindowProcedure
.
So, while that might seem to take care of everything, sorry, it doesn’t. In fact, if we try to use an instance of this type as the destination HWND, we’ll never see any messages appear in our managed window procedure.
Remember, all the windows we’ve been wrapping thus far have been existing windows managed by code independent of ours. This is the first time we’ve actually created our own, and we’re missing a rather essential component to the correct operation of a window, something I touched on briefly in the previous article.
That’s right; we need to man the pump: the window message pump! A window does nothing if its thread’s message queue isn’t being pumped in a loop.
But Doesn’t SendMessage Bypass the Message Queue?
Yes! Using the SendMessage
function (as we’re doing here) results in the message being sent to the window procedure, skipping the message queue. However, one needs to pay careful attention to the docs:
If the specified window was created by the calling thread, the window procedure is called immediately as a subroutine. If the specified window was created by a different thread, the system switches to that thread and calls the appropriate window procedure. Messages sent between threads are processed only when the receiving thread executes message retrieval code.
The bolded portion above is the most critical part. However, what exactly is “message retrieval code”? Simply put: a call to the GetMessage
function, itself part and parcel of a Win32 message loop. If this does not occur, no cross-thread message, even ones sent by SendMessage
, will be processed.
With that clarified, we need an object type that can provide the requisite message retrieval code, and the MessageOnlyExecutor
implementation of IThreadExecutor
fits the bill quite nicely.
Executors and Happy Wrappers
The MessageOnlyExecutor
, which uses a MessageOnlyWindowWrapper
instance, is an involved class that will handle dispatching messages while allowing us to execute operations and receive window messages on a dedicated thread.
MessageOnlyExecutor
was inspired by WPF’s Dispatcher
class and is quite involved — to learn how it works, please consult its source here.
It does many things above and beyond the minimum required for dispatching window messages; you could certainly whip up a stripped-down version. Regardless of what you do, we’ll need it for our hook procedures to talk back to us.
With the final requirement of our native functions satisfied, let’s get to P/Invoking.
P/Invoke Declarations
We spent all that time writing those native DLL exports; it’d be a shame to let them go to waste, so let us toss a P/Invoke function for each one in a new Hooks
class.
Hooks.cs
/// <summary> /// Provides interoperability with the Bad Echo Win32 hooks API. /// </summary> internal static partial class Hooks { private const string LIBRARY_NAME = "BadEcho.Hooks"; /// <summary> /// Installs a new Win32 hook procedure into the specified thread. /// </summary> /// <param name="hookType">The type of hook procedure to install.</param> /// <param name="threadId"> /// The identifier of the thread with which the hook procedure is to be associated. /// </param> /// <param name="destination"> /// A handle to the window that will receive messages sent to the hook procedure. /// </param> /// <returns>True if successful; otherwise, false.</returns> [LibraryImport(LIBRARY_NAME, SetLastError = true)] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] [return: MarshalAs(UnmanagedType.U1)] [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] public static partial bool AddHook(HookType hookType, int threadId, WindowHandle destination); /// <summary> /// Uninstalls a Win32 hook procedure from the specified thread. /// </summary> /// <param name="hookType">The type of hook procedure to uninstall.</param> /// <param name="threadId"> /// The identifier of the thread to remove the hook procedure from. /// </param> /// <returns>True if successful; otherwise, false.</returns> [LibraryImport(LIBRARY_NAME, SetLastError = true)] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] [return: MarshalAs(UnmanagedType.U1)] [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] public static partial bool RemoveHook(HookType hookType, int threadId); /// <summary> /// Changes the details of a hook message currently being intercepted. /// </summary> /// <param name="message">The message identifier to use.</param> /// <param name="wParam">Additional information about the message to use.</param> /// <param name="lParam">Additional information about the message to use.</param> [LibraryImport(LIBRARY_NAME)] [UnmanagedCallConv(CallConvs = new [] { typeof(CallConvCdecl)})] [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] public static partial void ChangeMessageDetails(uint message, IntPtr wParam, IntPtr lParam); }
Pretty much carbon copies of the C++ declarations, mixed with a bit of that ol’ type marshalling jazz.
Okay, time for the big boy: the GlobalWindowWrapper
.
The Out-Of-Process Window Wrapper
Alright, everything is in place for us to finally create our GlobalWindowWrapper
class, which is yet another derivation of WindowWrapper
.
GlobalWindowWrapper.cs
/// <summary> /// Provides a wrapper around an <c>HWND</c> of a provided out-of-process window and the messages /// it receives. /// </summary> /// <remarks> /// <para> /// Window messages intended for other applications cannot be intercepted via the usual technique /// of subclassing, as it is not possible to subclass a window created by and running on another /// process. /// </para> /// <para> /// Instead, several global hook procedures are installed and associated with the thread owning /// the wrapped window. This provides us with all pertinent window message traffic, even across /// process boundaries. Registering a callback for a global hook procedure requires more than /// just a function pointer to a managed delegate, however. Global hooks require the injection /// of a native, standard Windows DLL containing the hook procedure into a target process. /// </para> /// <para> /// Self-injection of this assembly isn't going to cut it, as a managed DLL simply cannot be /// loaded in an unmanaged environment, which will fail to load our not-so-standard DLLs due /// to the lack of a DllMain entry point. /// </para> /// <para> /// Luckily for us, or maybe just me, we have the native BadEcho.Hooks library, written in C++ /// and super injectable! If this DLL can be located for the necessary platform invokes, the /// necessary hooks are established and instances of said native DLL are loaded into the /// address space of our target processes. /// </para> /// <para> /// In order for our hooking DLL to be able to communicate back to our managed code, we create /// a message-only window that is set up to receive messages from unmanaged-land. /// So...there you go. Spy away! /// </para> /// </remarks> public sealed class GlobalWindowWrapper : WindowWrapper, IDisposable { private readonly MessageOnlyExecutor _hookExecutor = new(); private readonly int _threadId; private bool _disposed; private bool _windowHooked; /// <summary> /// Initializes a new instance of the <see cref="GlobalWindowWrapper"/> class. /// </summary> /// <param name="handle">A handle to the window being wrapped.</param> public GlobalWindowWrapper(WindowHandle handle) : base(handle) { _threadId = (int) User32.GetWindowThreadProcessId(Handle, IntPtr.Zero); if (_threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error()); } /// <inheritdoc/> public void Dispose() { if (_disposed) return; CloseHook(); _hookExecutor.Dispose(); _disposed = true; } /// <inheritdoc/> protected override void OnHookAdded(WindowHookProc addedHook) { base.OnHookAdded(addedHook); InitializeHook(); } /// <inheritdoc/> protected override void OnDestroyingWindow() { base.OnDestroyingWindow(); CloseHook(); } private async void InitializeHook() { if (_hookExecutor.Window != null) return; await _hookExecutor.RunAsync(); if (_hookExecutor.Window == null) throw new InvalidOperationException(Strings.MessageQueueForHookFailed); _hookExecutor.Window.AddHook(WindowProcedure); _windowHooked = Hooks.AddHook(HookType.CallWindowProcedure, _threadId, _hookExecutor.Window.Handle); } private void CloseHook() { if (!_windowHooked) return; _windowHooked = !Hooks.RemoveHook(HookType.CallWindowProcedure, _threadId); if (_windowHooked) Logger.Warning(Strings.UnhookWindowFailed.InvariantFormat(_threadId)); } }
When the external window is about to receive a message, our hook procedure will send said message to the MessageOnlyWindowWrapper.WindowProcedure
being powered by our local MessageOnlyExecutor
. Because our GlobalWindowWrapper
added a hook to the message-only window, it will be passed along to the GlobalWindowWrapper.WindowProcedure
, which will then of course share it with its own registered hooks.
In other words, mission complete! This code will likely have some features and needed contingencies added to it, but it does the job in the tests I’ve run. You can find the up-to-date source for this class among its brethren at the ol’ Bad Echo repository.
In the final article in this series of window message interception using managed code, we’ll be taking a look at how, through another implementation of IMessageSource<T>
, we can add support for the WH_GETMESSAGE
hook type, which features a different hook procedure signature and supports mutable message details.