0

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.

junvar
  • 11,151
  • 2
  • 30
  • 46
  • If it matters, `x` seems to be pointing to address 0; `Console.WriteLine("3 [" + x + "]");` writes `3 [0]`. – junvar Apr 04 '19 at 14:33
  • My guess, having used this same code in an app, is that the delegate is being disposed. Add `GC.KeepAlive(HookProc);` after you call `SetWindowsHookEx` - as I had to do [here](https://github.com/stuartd/keymapper/blob/master/keymapper/Classes/KeySniffer.cs#L80) – stuartd Apr 04 '19 at 14:34
  • Have you considered building this in visual studio and debugging it? – Maximilian Burszley Apr 04 '19 at 14:34
  • @stuartd, `GC.KeepAlive(HookProc);` gives me the compilation error `'KeyLogger.Program.HookProx' is a 'type' but is used like a 'variable'`. But I think my problem may be related to GC, because it seems more quick to occur when I have something big running. – junvar Apr 04 '19 at 14:39
  • So it does. Hmm.. One sec- – stuartd Apr 04 '19 at 14:41
  • Also interesting, I tried adding some `Console.WriteLine`s in the `Begin` function. And depending on how many I added or where I added them, the script would work as currently, stop midway in the `Begin` function and pause all running instances, or give a runtime error. – junvar Apr 04 '19 at 14:48

1 Answers1

1

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


stuartd
  • 70,509
  • 14
  • 132
  • 163
  • Looking at this, if the proc is referenced in a field I don't think it would be disposed. But when I had to do this it was more than a decade ago, and at the time it "worked" so I didn't dig in any further. – stuartd Apr 04 '19 at 14:52
  • I don't want to jump the gun too soon, but it *seems* 1) moving the `proc` assignment before calling `SetHook`, and then 2) passing `proc` instead of `HookCallback` as the 2nd param of `SetWindowsHookEx` in `SetHook` may have resolved the issue. You're right that `GC.KeepAlive` seems to be unnecessary. – junvar Apr 04 '19 at 15:29