Managed Interception of Message Queue Messages Using Win32 Hooks

Hello and welcome to what will be the final article in a series that looks at the interception of window messages with managed code. The first two articles dealt with intercepting messages bound for a window procedure (for both local and external windows); this final one will deal with grabbing messages within a message loop.

The message queue hook procedure differs from the one used in the previous articles, so we’ll need to provide another implementation of IMessageSource<THookProc>, with the enclosing type being GetMessageHookProc.

Message queue hooks are somewhat similar to window procedure hooks; however, there are some crucial differences, including support for mutable message details.

We already added support in the previous article for WH_GETMESSAGE in our Native DLL, so this will probably (and hopefully) be a shorter write (for me) and read (for you).

The Hook Procedure for GetMessage

In the two previous articles, we used the WindowHookProc callback delegate, whose signature is compatible with what the WH_CALLWNDPROC and WH_CALLWNDPROCRET hook types expect.

While the WH_GETMESSAGE hook procedure has a signature that looks identical to WH_CALLWNDPROC/WH_CALLWNDPROCRET, there is one big difference: the message details (pointed to by lParam) are mutable.

I don’t know about you, but changing the details of messages about to be pumped through another process’s message queue sounds like a great way to inject a ton of chaos to me. So of course, we must add support for this!

GetMessageHookProc.cs
/// <summary>
/// Represents a callback that receives messages about to be returned from a message queue.
/// </summary>
/// <param name="hWnd">A handle to the window.</param>
/// <param name="msg">The message.</param>
/// <param name="wParam">Additional message-specific information.</param>
/// <param name="lParam">Additional message-specific information.</param>
/// <returns>
/// The result of the message processing, which of course depends on the message being processed.
/// </returns>
public delegate HookResult GetMessageHookProc(IntPtr hWnd, 
                                              ref uint msg, 
                                              ref IntPtr wParam, 
                                              ref IntPtr lParam);

You might take a look at the delegate above and say, “Boy, that’s a lot ref parameters.” You would be correct friend!

All these parameters can be changed by a hook procedure installed in the hook chain. Let us make this possible in .NET-land by creating a new implementation of IMessageSource<THookProc> specifically for WH_GETMESSAGE hooks.

Creating a Message Queue Message Source

If you read the first article, you will recall the WindowWrapper type and implementation of IMessageSource<WindowHookProc>, and it is from this type that all our subsequent window hooking types are derived.

We will be taking a similar approach with messages queues; however, this class will be sealed and self-contained, given the singular nature of the hook itself.

Additionally, there is no need to wrap any sort of unmanaged handle, be it an HWND or whatnot, with message queues; all we need is a thread ID.

So it’ll look a little bit different.

MessageQueueMessageSource.cs
/// <summary>
/// Provides a publisher of messages being read from a message queue.
/// </summary>
public sealed class MessageQueueMessageSource : IMessageSource<GetMessageHookProc>, IDisposable
{
    private readonly CachedWeakList<GetMessageHookProc> _hooks = new();
    private readonly MessageOnlyExecutor _hookExecutor = new();
    private readonly int _threadId;

    private bool _disposed;
    private bool _queueHooked;

    /// <summary>
    /// Initializes a new instance of the <see cref="MessageQueueMessageSource"/> class.
    /// </summary>
    /// <param name="threadId">
    /// The identifier for the thread whose message pump we're hooking.
    /// </param>
    public MessageQueueMessageSource(int threadId)
    {
        _threadId = threadId;
    }

    /// <inheritdoc/>
    public void AddHook(GetMessageHookProc hook)
    {
        Require.NotNull(hook, nameof(hook));

        _hooks.Add(hook);

        InitializeHook();
    }

    /// <inheritdoc/>
    public void RemoveHook(GetMessageHookProc hook)
    {
        Require.NotNull(hook, nameof(hook));

        _hooks.Remove(hook);
    }

    /// <inheritdoc/>
    public void Dispose()
    {
        if (_disposed)
            return;

        if (_queueHooked)
        {
            _queueHooked = !Hooks.RemoveHook(HookType.GetMessage, _threadId);

            if (_queueHooked)
                Logger.Warning(Strings.UnhookMessageQueueFailed.InvariantFormat(_threadId));
        }

        _hookExecutor.Dispose();

        _disposed = true;
    }

    private async void InitializeHook()
    {
        if (_hookExecutor.Window != null)
            return;
         
        await _hookExecutor.RunAsync();
            
        if (_hookExecutor.Window == null)
            throw new InvalidOperationException(Strings.MessageQueueForHookFailed);

        _hookExecutor.Window.AddHook(GetMessageProcedure);

        _queueHooked = Hooks.AddHook(HookType.GetMessage,
                                     _threadId,
                                     _hookExecutor.Window.Handle);
    }

    private HookResult GetMessageProcedure(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
    {
        uint localMsg = msg;
        IntPtr localWParam = wParam;
        IntPtr localLParam = lParam;

        foreach (GetMessageHookProc hook in _hooks)
        {
            var result = hook(hWnd, ref localMsg, ref localWParam, ref localLParam);

            if (result.Handled)
                break;
        }
        
        if (localMsg != msg || localWParam != wParam || localLParam != lParam)
            Hooks.ChangeMessageDetails(localMsg, localWParam, localLParam);

        // We always mark it as handled, we don't want further processing by any supporting
        // infrastructure.
        return new HookResult(IntPtr.Zero, true);
    }
}

The above code will relay all messages about to be returned from a GetMessage function call to our code and allow us to make any changes we wish to the message’s details.

You’ll notice that this class also uses a MessageOnlyExecutor to receive the inbound hook messages from our native DLL, much like GlobalWindowWrapper did. Refer to the second article in this series for more information on the role of this class.

And they said you couldn’t use Win32 hooks in .NET! Pfft.

Other Hook Types and Going Global

We can easily add support for all the other various hook types by following a similar approach as demonstrated above.

If we want to create global, system-wide hooks (very dangerous and fun), we’d need to have some additional P/Invoke functions that would do the same stuff as AddHook but without specifying the thread ID.

Some hook types either only support global installations or are only helpful when used in a global context. So I’ll eventually add this capability to the Bad Echo codebase, and you should be able to add it to yours easily; no article on the matter needs to be written.

Messages: Intercepted!

This concludes the three-part series on intercepting window messages from .NET code. Hooks are an interesting Windows programming topic, so I hope you found the information shared as enjoyable as I did.

Yay, we're done.

The majority of the code mentioned in these articles can be found on the Bad Echo core technologies source repository.

Have fun, and don’t do anything you’re not supposed to with this code! This was probably the shortest article I’ve ever written; I should do it more often.