Search code examples
c#windowspowershellkeyloggerkeyhook

Powershell / C# keyhook script NullReferenceException externally


I'm trying to use a powershell script (using C#) to listen to key strokes. It works a short time before erroring out. What's interesting is after I added some console writes, it seems the error is occurring external to my code.

Here's the powreshell script:

Add-Type -TypeDefinition @"
    using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;

    namespace KeyLogger {
        public static class Program {
            private const int WH_KEYBOARD_LL = 13;
            private static IntPtr hookId = IntPtr.Zero;

            public static void Begin() {
                hookId = SetHook();
                Application.Run();
                UnhookWindowsHookEx(hookId);
            }

            private static IntPtr SetHook() {
                IntPtr moduleHandle = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);
                return SetWindowsHookEx(WH_KEYBOARD_LL, HookCallback, moduleHandle, 0);
            }

            private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
                Console.WriteLine('0');
                if (nCode == 0) {
                    Console.WriteLine('1');
                    Console.WriteLine(Marshal.ReadInt32(lParam) + " " + wParam + " ");
                }
                Console.WriteLine('2');
                IntPtr x = CallNextHookEx(hookId, nCode, wParam, lParam);
                Console.WriteLine('3');
                return x;
            }

            private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

            [DllImport("user32.dll")]
            private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
            [DllImport("user32.dll")]
            private static extern bool UnhookWindowsHookEx(IntPtr hhk);
            [DllImport("user32.dll")]
            private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
            [DllImport("kernel32.dll")]
            private static extern IntPtr GetModuleHandle(string lpModuleName);
        }
    }
"@ -ReferencedAssemblies System.Windows.Forms

[KeyLogger.Program]::Begin();

The error is:

An error has occurred that was not properly handled. Additional information is shown below. The Windows PowerShell process will exit. Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object.

Notice the Console.WriteLine 's in the HookCallback method. But the last debug line printed is 3, indicating the error occurs outside of HookCallback. What's even more interesting, if I have multiple instances of the script running, they don't all error simultaneously; one may crash, but the remaining continue on for a bit more.

I'm not too familiar with powershell or C#, so am wondering how to get more debug information? I would expect a line number or file or library name where the error occurred, but am not sure how to get that information.

Disclaimer, I took the script from here https://blogs.msdn.microsoft.com/toub/2006/05/03/low-level-keyboard-hook-in-c/ (modified slightly).

Edit: I've accepted Stuartd's answer as it was very helpful, but as I said in the comments, it required small modifications to work. For ease to future readers, here are the modifications that worked:

 private static HookProc callback;
 ...

 private static IntPtr SetHook() {
     callback = HookCallback;
     IntPtr moduleHandle = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);
     return SetWindowsHookEx(WH_KEYBOARD_LL, callback, moduleHandle, 0);
 }
 ...

I've done more searching and reading up on C#, and it seems the cause was that SetWindowsHookEx does not manage / keep alive it's hookProc delegate parameter, and passing a static method directly creates a local (local to the method) delegate that is ready for deletion once the SetHook method returns.


Solution

  • This might do it: create a field that refers to HookProc, and tell GC to keep it alive:

    public static class Program
    {
        private const int WH_KEYBOARD_LL = 13;
        private static IntPtr hookId = IntPtr.Zero;
        private static HookProc proc;
    
        public static void Begin()
        {
            proc = HookCallback;
            hookId = SetHook();
            GC.KeepAlive(proc);
            Application.Run();
            UnhookWindowsHookEx(hookId);
        }
    
         … etc