Mirror

How to create a system wide windows hook (Views: 716)


Problem/Question/Abstract:

How to create a system wide windows hook

Answer:

The following example demonstrates creating a system wide windows hook under Win32. The example provides both the code for the system hook dll and an example application. The hook function that we will create will also demonstrate advanced coding techniques such as sharing global memory across process boundaries using memory mapped files, sending messages from the key hook function back to the originating application, and dynamic loading of a dll at runtime.

The example keyboard hook that we create will keep a count of the number of keystrokes a user enters on the keyboard. Further, we will demonstrate trapping the enter key, and passing a message back to the application that initiated the keyboard hook each time the enter key is pressed. Finally, we will demonstrate trapping the left arrow key and instead of letting it through to the current application, we will instead replace it with a right arrow keystroke. (Note: that this can cause much confusion to a unsuspecting user).

Adding a hook to the windows system involves calling the Windows API function SetWindowsHookEx() and passing it the type of hook you wish to install, and address of the hook function you are installing. System wide hook functions are required to reside in a dynamic link library, since they must be mapped into each process on the system. The SetWindowsHookEx() function adds your hook function into the Windows "hook chain", returning a handle (or id) of the hook you are installing. You will use this handle to identify your hook to windows, and to remove your hook when you are done trapping the keyboard.

The Windows "hook chain" is a linked list of functions that Windows uses to keep track of all the installed hooks, allowing multiple hooks to be installed at any given time. Occasionally, Windows will ask your hook function to call the next hook in the chain, allowing all the hooks an opportunity to function. When we do call the next hook in the chain, we will need to identify ourselves by passing the handle of our hook function to the next hook.

Creating a Windows hook requires special handling under Win32, since the dll must be mapped (on the fly) into the process space of every application that receives keystrokes. Normally, this is not an issue, however, when operating inside a keyhook procedure, global variables (such as your hook handle) must be preserved while the dll is mapped into other process spaces. Under Win16, this would not be a program, since dlls had a single data segment that was shared across all process mappings. Under Win32, each mapping of the dll receives its own data segment. This means that as the dll that contains the keyboard hook is mapped into each process that receives keystrokes, it receives a new data segment, and new unitialized variables with it. This is a problem, since global variables (such as your hook handle) must be preserved across process mappings. To solve this problem, we will take advantage of Win32's ability to memory map variables from the system paging file.

Each time our dll is mapped into a process, the DllMain() function in our dll will be called by windows, with a parameter flag indicating the reason for the call. When we receive the DLL_PROCESS_ATTACH flag (indicating our dll is getting mapped into a different process), we will create a file mapping to the system paging file and get a pointer to our memory mapped variables. When we receive the DLL_PROCESS_DETACH flag (indicating our dll is getting un-mapped from a process), we will free our file mapping of the system paging file. The variables we will need to keep track of (and have access to from both the dll and the application that originally loaded the keyboard hook) are placed in a record structure called THookRec. The THookRec structure has the following fields:



TheHookHandle:

The handle (id) of the Keyboard hook that we set. We will need access to this variable during the execution of the keyhook function, to identify ourselves to windows when we are asked to call the next hook in the hook chain. We will also need access to this variable when we remove our hook. Finally, the originating application that will receive the messages from our hook function can access this variable to see if and when the hook is active.



TheAppWinHandle:

While this variable is not used in our example dll or application, it is a starting place for adding additional messaging capabilities between the hook function and your application that initiates the hook. It can also be useful for determining if the hook is functioning while mapped into the context of the initiating application.



TheCtrlWinHandle:

This variable will hold the handle to a button control in our initiating application. We will use this handle to send messages from the keyboard hook function to the button control. Every time the enter key is pressed, we will send a WM_KEYDOWN and a WM_KEYUP message to the button and a key value of 0 (zero). We will trap the OnKeyDown event in the button control, and keep count of the number of times the user presses the enter key.



TheKeyCount:

This variable will keep track of the total number of key presses made by the user. Obviously our keyhook will need access to this variable to increment its value, and the originating application that will receive the messages from our hook function will want to access this variable to display real time results.



The DLL contains the following functions:

MapFileMemory:
Creates a system paging file mapping object and initializes a pointer to our mapping variable of type THookRec.

UnMapFileMemory:
Frees the system paging file mapping object and mapping variable created by the MapFileMemory() function.

GetHookRecPointer:
An exported function that returns a pointer to the mapping variable created by the MapFileMemory() function. The initiating application can both set and examine this memory block, and effectively share memory that is used by our hook function during the time the hook function is operating in the context of another process space.

KeyBoardProc:
The actual hook function. This function receives both keydown, and keyup messages as well as a message from windows indicating we should call the next hook in the windows "hook chain". This function increments TheKeyCount field of the memory mapped THookRec structure if the keystroke we are processing is a keyup message. If the key being processed is the enter key, we will fire the OnKeyDown event of the window provided in "TheCtrlWinHandle" field of the memory mapped THookRec structure. Finally, if the left arrow key is pressed, we will swallow the keystroke, and instead send a right arrow key stroke to the application. Note that the following variables and initializing code has been included in this function for your convience. The variables have been commented out in the code (as not to compile). To use them, simply remove the comments in the code:

IsAltPressed  {Determines if the Alt key is currently down}
IsCtrlPressed  {Determines if the Control key is currently down}
IsShiftPressed  {Determines if the Shift key is currently down}


StartKeyBoardHook:
An exported function that allows the application to initiate installing the keyboard hook.

StopKeyBoardHook:
An exported function that allows the application to initiate removing the keyboard hook.

DllEntryPoint:
The main entry point into our dll, allowing us to know when our dll is being mapped in, and out of, different application's address space.



Delphi Hook DLL Example:



library TheHook;

uses
  Windows, Messages, SysUtils;

{Define a record for recording and passing information process wide}
type
  PHookRec = ^THookRec;
  THookRec = packed record
    TheHookHandle: HHOOK;
    TheAppWinHandle: HWND;
    TheCtrlWinHandle: HWND;
    TheKeyCount: DWORD;
  end;

var
  hObjHandle: THandle; {Variable for the file mapping object}
  lpHookRec: PHookRec; {Pointer to our hook record}

procedure MapFileMemory(dwAllocSize: DWORD);
begin
  {Create a process wide memory mapped variable}
  hObjHandle := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0, dwAllocSize,
    'HookRecMemBlock');
  if (hObjHandle = 0) then
  begin
    MessageBox(0, 'Hook DLL', 'Could not create file map object', MB_OK);
    exit;
  end;
  {Get a pointer to our process wide memory mapped variable}
  lpHookRec := MapViewOfFile(hObjHandle, FILE_MAP_WRITE, 0, 0, dwAllocSize);
  if (lpHookRec = nil) then
  begin
    CloseHandle(hObjHandle);
    MessageBox(0, 'Hook DLL', 'Could not map file', MB_OK);
    exit;
  end;
end;

procedure UnMapFileMemory;
begin
  {Delete our process wide memory mapped variable}
  if (lpHookRec <> nil) then
  begin
    UnMapViewOfFile(lpHookRec);
    lpHookRec := nil;
  end;
  if (hObjHandle > 0) then
  begin
    CloseHandle(hObjHandle);
    hObjHandle := 0;
  end;
end;

function GetHookRecPointer: pointer stdcall;
begin
  {Return a pointer to our process wide memory mapped variable}
  result := lpHookRec;
end;

{The function that actually processes the keystrokes for our hook}

function KeyBoardProc(Code: integer; wParam: integer; lParam: integer): integer;
  stdcall;
var
  KeyUp: bool;
  {Remove comments for additional functionability ... :

  IsAltPressed: bool;
  IsCtrlPressed: bool;
  IsShiftPressed: bool;
  }
begin
  result := 0;
  case Code of
    HC_ACTION:
      begin
        {We trap the keystrokes here}
        {Is this a key up message?}
        KeyUp := ((lParam and (1 shl 31)) <> 0);

        {Remove comments for additional functionability ... :

        {Is the Alt key pressed}
        if ((lParam and (1 shl 29)) <> 0) then
        begin
          IsAltPressed := TRUE;
        end
        else
        begin
          IsAltPressed := FALSE;
        end;
        {Is the Control key pressed}
        if ((GetKeyState(VK_CONTROL) and (1 shl 15)) <> 0) then
        begin
          IsCtrlPressed := TRUE;
        end
        else
        begin
          IsCtrlPressed := FALSE;
        end;
        {if the Shift key pressed}
        if ((GetKeyState(VK_SHIFT) and (1 shl 15)) <> 0) then
        begin
          IsShiftPressed := TRUE;
        end
        else
        begin
          IsShiftPressed := FALSE;
        end;
        }

        {If KeyUp then increment the key count}
        if (KeyUp <> FALSE) then
        begin
          Inc(lpHookRec^.TheKeyCount);
        end;
        case wParam of
          {Was the enter key pressed?}
          VK_RETURN:
            begin
              {if KeyUp}
              if (KeyUp <> FALSE) then
              begin
                {Post a bogus message to the window control in our app}
                PostMessage(lpHookRec^.TheCtrlWinHandle, WM_KEYDOWN, 0, 0);
                PostMessage(lpHookRec^.TheCtrlWinHandle, WM_KEYUP, 0, 0);
              end;
              {If you wanted to swallow the keystroke then return -1, else if you want
                                                        to allow the keystroke then return 0}
              result := 0;
              exit;
            end; {VK_RETURN}
          {If the left arrow key is pressed then lets play a joke!}
          VK_LEFT:
            begin
              {if KeyUp}
              if (KeyUp <> FALSE) then
              begin
                {Create a UpArrow keyboard event}
                keybd_event(VK_RIGHT, 0, 0, 0);
                keybd_event(VK_RIGHT, 0, KEYEVENTF_KEYUP, 0);
              end;
              {Swallow the keystroke}
              result := -1;
              exit;
            end; {VK_LEFT}
        end; {case wParam}
        {Allow the keystroke}
        result := 0;
      end; {HC_ACTION}
    HC_NOREMOVE:
      begin
        {This is a keystroke message, but the keystroke message has not been removed
                         from the message queue, since an application has called PeekMessage()
                        specifying PM_NOREMOVE}
        result := 0;
        exit;
      end;
  end; {case code}
  if (Code < 0) then
    {Call the next hook in the hook chain}
    result := CallNextHookEx(lpHookRec^.TheHookHandle, Code, wParam, lParam);
end;

procedure StartKeyBoardHook stdcall;
begin
  {If we have a process wide memory variable and the hook has not already been set...}
  if ((lpHookRec <> nil) and (lpHookRec^.TheHookHandle = 0)) then
  begin
    {Set the hook and remember our hook handle}
    lpHookRec^.TheHookHandle := SetWindowsHookEx(WH_KEYBOARD, @KeyBoardProc,
      hInstance, 0);
  end;
end;

procedure StopKeyBoardHook stdcall;
begin
  {If we have a process wide memory variable and the hook has already been set...}
  if ((lpHookRec <> nil) and (lpHookRec^.TheHookHandle <> 0)) then
  begin
    {Remove our hook and clear our hook handle}
    if (UnHookWindowsHookEx(lpHookRec^.TheHookHandle) <> FALSE) then
    begin
      lpHookRec^.TheHookHandle := 0;
    end;
  end;
end;

procedure DllEntryPoint(dwReason: DWORD);
begin
  case dwReason of
    Dll_Process_Attach:
      begin
        {If we are getting mapped into a process, then get a pointer to our
                                process wide memory mapped variable}
        hObjHandle := 0;
        lpHookRec := nil;
        MapFileMemory(sizeof(lpHookRec^));
      end;
    Dll_Process_Detach:
      begin
        {If we are getting unmapped from a process then, remove the pointer to
                                our process wide memory mapped variable}
        UnMapFileMemory;
      end;
  end;
end;

exports
  KeyBoardProc name 'KEYBOARDPROC',
  GetHookRecPointer name 'GETHOOKRECPOINTER',
  StartKeyBoardHook name 'STARTKEYBOARDHOOK',
  StopKeyBoardHook name 'STOPKEYBOARDHOOK';

begin
  {Set our Dll's main entry point}
  DLLProc := @DllEntryPoint;
  {Call our Dll's main entry point}
  DllEntryPoint(Dll_Process_Attach);

end.


Application notes:

The test application we have created demonstrates loading the dll that contains the keyboard hook, installing the keyboard hook, displaying the total keystroke count and the number of times the enter key has been pressed (in real time), uninstalling the keyboard hook and unloading the dll.

The application code starts out by defining a form containing two labels, a button, and timer component. Once we install our hook function, we will start the timer, and upon every timer event, we will display in label1 the total number of keystrokes that have been entered by the user since the hook was set. The hook will also fire the button's OnKeyDown event each time the enter key is pressed, giving us the opportunity to display the total number of times the enter key has been pressed in the caption of label2.

After the form is defined, we then define the THookRec structure in the same manner as it is defined in the hook dll. Other variables we will use include: a handle variable used for loading the hook dll, and three function pointer variables used to call the GetHookRecPointer(), StartKeyBoardHook(), and StopKeyBoardHook() functions. Finally we define a pointer to a THookRec structure used to access the memory mapped variables used by the hook function, a variable to keep track of the number of times the enter key is pressed, and a variable used to indicate the success of loading the dll, getting its functions, and setting the hook.



The application logic goes something like this:

On form create, we will initialize our form's components, attempt to dynamically load the hook dll, and get the address of the GetHookRecPointer(), StartKeyBoardHook(), and StopKeyBoardHook() functions located in the hook dll. If we are successful, we will retrieve a pointer to THookRec structure used by the hook dll, we will then initialize structure, adding the handle of the button control so the keyboard hook will know which window control to call when the enter key is pressed. We will then attempt to start the keyboard hook. If we are successful, at setting the hook, we can then start the timer.

On form destroy, if we where previously successful in installing the windows hook and loading the hook dll, we will now uninstall the windows hook, and unload the KeyHook dll.

On the timer's timer event, we will simply display the total number of key presses in the form's label1 caption by accessing the KeyHook dll's THookRec structure.

On the Buttons KeyDown event, if the key value passed is zero we increment our EnterKeyCount variable and display the total number of times the enter key has been pressed by accessing the KeyHook dll's THookRec structure.


Delphi TestApp Example:


unit TestHk1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls,
    ExtCtrls;

type
  TForm1 = class(TForm)
    Label1: TLabel;
    Label2: TLabel;
    Timer1: TTimer;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure Button1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

{Functions prototypes for the hook dll}
type
  TGetHookRecPointer = function: pointer stdcall;
type
  TStartKeyBoardHook = procedure stdcall;
type
  TStopKeyBoardHook = procedure stdcall;

  {The record type filled in by the hook dll}
type
  THookRec = packed record
    TheHookHandle: HHOOK;
    TheAppWinHandle: HWND;
    TheCtrlWinHandle: HWND;
    TheKeyCount: DWORD;
  end;

  {A pointer type to the hook record}
type
  PHookRec = ^THookRec;

var
  hHookLib: THandle; {A handle to the hook dll}
  GetHookRecPointer: TGetHookRecPointer; {Function pointer}
  StartKeyBoardHook: TStartKeyBoardHook; {Function pointer}
  StopKeyBoardHook: TStopKeyBoardHook; {Function pointer}
  LibLoadSuccess: bool; {If the hook lib was successfully loaded}
  lpHookRec: PHookRec; {A pointer to the hook record}
  EnterKeyCount: DWORD; {An internal count of the Enter Key}

procedure TForm1.FormCreate(Sender: TObject);
begin
  {Set our initial variables}
  Timer1.Enabled := FALSE;
  Timer1.Interval := 1000;
  Label1.Caption := '0 Keys Logged';
  Label2.Caption := '0 Enter Keys Logged';
  EnterKeyCount := 0;
  lpHookRec := nil;
  LibLoadSuccess := FALSE;
  @GetHookRecPointer := nil;
  @StartKeyBoardHook := nil;
  @StopKeyBoardHook := nil;
  {Try to load the hook dll}
  hHookLib := LoadLibrary('THEHOOK.DLL');
  {If the hook dll was loaded successfully}
  if hHookLib <> 0 then
  begin
    {Get the function addresses}
    @GetHookRecPointer := GetProcAddress(hHookLib, 'GETHOOKRECPOINTER');
    @StartKeyBoardHook := GetProcAddress(hHookLib, 'STARTKEYBOARDHOOK');
    @StopKeyBoardHook := GetProcAddress(hHookLib, 'STOPKEYBOARDHOOK');
    {Did we find all the functions we need?}
    if ((@GetHookRecPointer <> nil) and (@StartKeyBoardHook <> nil) and
      (@StopKeyBoardHook <> nil)) then
    begin
      LibLoadSuccess := TRUE;
      {Get a pointer to the hook record}
      lpHookRec := GetHookRecPointer;
      {Were we successfull in getting a ponter to the hook record}
      if (lpHookRec <> nil) then
      begin
        {Fill in our portion of the hook record}
        lpHookRec^.TheHookHandle := 0;
        lpHookRec^.TheCtrlWinHandle := Button1.Handle;
        lpHookRec^.TheKeyCount := 0;
        {Start the keyboard hook}
        StartKeyBoardHook;
        {Start the timer if the hook was successfully set}
        if (lpHookRec^.TheHookHandle <> 0) then
        begin
          Timer1.Enabled := TRUE;
        end;
      end;
    end
    else
    begin
      {We failed to find all the functions we need}
      FreeLibrary(hHookLib);
      hHookLib := 0;
      @GetHookRecPointer := nil;
      @StartKeyBoardHook := nil;
      @StopKeyBoardHook := nil;
    end;
  end;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  {Did we load the dll successfully?}
  if (LibLoadSuccess = TRUE) then
  begin
    {Did we sucessfully get a pointer to the hook record?}
    if (lpHookRec <> nil) then
    begin
      {Did the hook get set?}
      if (lpHookRec^.TheHookHandle <> 0) then
      begin
        Timer1.Enabled := FALSE;
        StopKeyBoardHook;
      end;
    end;
    {Free the hook dll}
    FreeLibrary(hHookLib);
  end;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  {Display the number of keystrokes logged}
  Label1.Caption := IntToStr(lpHookRec^.TheKeyCount) + ' Keys Logged';
end;

procedure TForm1.Button1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  {Process message sent from hook dll and display number of time the enter key was pressed}
  if (Key = 0) then
  begin
    Inc(EnterKeyCount);
    Label2.Caption := IntToStr(EnterKeyCount) + ' Enter Keys Logged';
  end;
end;

end.

<< Back to main page