Author: Jonas Bilinkevicius
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:
1 library TheHook;
2
3 uses
4 Windows, Messages, SysUtils;
5
6 {Define a record for recording and passing information process wide}
7 type
8 PHookRec = ^THookRec;
9 THookRec = packed record
10 TheHookHandle: HHOOK;
11 TheAppWinHandle: HWND;
12 TheCtrlWinHandle: HWND;
13 TheKeyCount: DWORD;
14 end;
15
16 var
17 hObjHandle: THandle; {Variable for the file mapping object}
18 lpHookRec: PHookRec; {Pointer to our hook record}
19
20 procedure MapFileMemory(dwAllocSize: DWORD);
21 begin
22 {Create a process wide memory mapped variable}
23 hObjHandle := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0, dwAllocSize,
24 'HookRecMemBlock');
25 if (hObjHandle = 0) then
26 begin
27 MessageBox(0, 'Hook DLL', 'Could not create file map object', MB_OK);
28 exit;
29 end;
30 {Get a pointer to our process wide memory mapped variable}
31 lpHookRec := MapViewOfFile(hObjHandle, FILE_MAP_WRITE, 0, 0, dwAllocSize);
32 if (lpHookRec = nil) then
33 begin
34 CloseHandle(hObjHandle);
35 MessageBox(0, 'Hook DLL', 'Could not map file', MB_OK);
36 exit;
37 end;
38 end;
39
40 procedure UnMapFileMemory;
41 begin
42 {Delete our process wide memory mapped variable}
43 if (lpHookRec <> nil) then
44 begin
45 UnMapViewOfFile(lpHookRec);
46 lpHookRec := nil;
47 end;
48 if (hObjHandle > 0) then
49 begin
50 CloseHandle(hObjHandle);
51 hObjHandle := 0;
52 end;
53 end;
54
55 function GetHookRecPointer: pointer stdcall;
56 begin
57 {Return a pointer to our process wide memory mapped variable}
58 result := lpHookRec;
59 end;
60
61 {The function that actually processes the keystrokes for our hook}
62
63 function KeyBoardProc(Code: integer; wParam: integer; lParam: integer): integer;
64 stdcall;
65 var
66 KeyUp: bool;
67 {Remove comments for additional functionability ... :
68
69 IsAltPressed: bool;
70 IsCtrlPressed: bool;
71 IsShiftPressed: bool;
72 }
73 begin
74 result := 0;
75 case Code of
76 HC_ACTION:
77 begin
78 {We trap the keystrokes here}
79 {Is this a key up message?}
80 KeyUp := ((lParam and (1 shl 31)) <> 0);
81
82 {Remove comments for additional functionability ... :
83
84 {Is the Alt key pressed}
85 if ((lParam and (1 shl 29)) <> 0) then
86 begin
87 IsAltPressed := TRUE;
88 end
89 else
90 begin
91 IsAltPressed := FALSE;
92 end;
93 {Is the Control key pressed}
94 if ((GetKeyState(VK_CONTROL) and (1 shl 15)) <> 0) then
95 begin
96 IsCtrlPressed := TRUE;
97 end
98 else
99 begin
100 IsCtrlPressed := FALSE;
101 end;
102 {if the Shift key pressed}
103 if ((GetKeyState(VK_SHIFT) and (1 shl 15)) <> 0) then
104 begin
105 IsShiftPressed := TRUE;
106 end
107 else
108 begin
109 IsShiftPressed := FALSE;
110 end;
111 }
112
113 {If KeyUp then increment the key count}
114 if (KeyUp <> FALSE) then
115 begin
116 Inc(lpHookRec^.TheKeyCount);
117 end;
118 case wParam of
119 {Was the enter key pressed?}
120 VK_RETURN:
121 begin
122 {if KeyUp}
123 if (KeyUp <> FALSE) then
124 begin
125 {Post a bogus message to the window control in our app}
126 PostMessage(lpHookRec^.TheCtrlWinHandle, WM_KEYDOWN, 0, 0);
127 PostMessage(lpHookRec^.TheCtrlWinHandle, WM_KEYUP, 0, 0);
128 end;
129 {If you wanted to swallow the keystroke then return -1, else if you
130 want
131 to allow the keystroke then return 0}
132 result := 0;
133 exit;
134 end; {VK_RETURN}
135 {If the left arrow key is pressed then lets play a joke!}
136 VK_LEFT:
137 begin
138 {if KeyUp}
139 if (KeyUp <> FALSE) then
140 begin
141 {Create a UpArrow keyboard event}
142 keybd_event(VK_RIGHT, 0, 0, 0);
143 keybd_event(VK_RIGHT, 0, KEYEVENTF_KEYUP, 0);
144 end;
145 {Swallow the keystroke}
146 result := -1;
147 exit;
148 end; {VK_LEFT}
149 end; {case wParam}
150 {Allow the keystroke}
151 result := 0;
152 end; {HC_ACTION}
153 HC_NOREMOVE:
154 begin
155 {This is a keystroke message, but the keystroke message has not been removed
156 from the message queue, since an application has called PeekMessage()
157 specifying PM_NOREMOVE}
158 result := 0;
159 exit;
160 end;
161 end; {case code}
162 if (Code < 0) then
163 {Call the next hook in the hook chain}
164 result := CallNextHookEx(lpHookRec^.TheHookHandle, Code, wParam, lParam);
165 end;
166
167 procedure StartKeyBoardHook stdcall;
168 begin
169 {If we have a process wide memory variable and the hook has not already been
170 set...}
171 if ((lpHookRec <> nil) and (lpHookRec^.TheHookHandle = 0)) then
172 begin
173 {Set the hook and remember our hook handle}
174 lpHookRec^.TheHookHandle := SetWindowsHookEx(WH_KEYBOARD, @KeyBoardProc,
175 hInstance, 0);
176 end;
177 end;
178
179 procedure StopKeyBoardHook stdcall;
180 begin
181 {If we have a process wide memory variable and the hook has already been set...}
182 if ((lpHookRec <> nil) and (lpHookRec^.TheHookHandle <> 0)) then
183 begin
184 {Remove our hook and clear our hook handle}
185 if (UnHookWindowsHookEx(lpHookRec^.TheHookHandle) <> FALSE) then
186 begin
187 lpHookRec^.TheHookHandle := 0;
188 end;
189 end;
190 end;
191
192 procedure DllEntryPoint(dwReason: DWORD);
193 begin
194 case dwReason of
195 Dll_Process_Attach:
196 begin
197 {If we are getting mapped into a process, then get a pointer to our
198 process wide memory mapped variable}
199 hObjHandle := 0;
200 lpHookRec := nil;
201 MapFileMemory(sizeof(lpHookRec^));
202 end;
203 Dll_Process_Detach:
204 begin
205 {If we are getting unmapped from a process then, remove the pointer to
206 our process wide memory mapped variable}
207 UnMapFileMemory;
208 end;
209 end;
210 end;
211
212 exports
213 KeyBoardProc name 'KEYBOARDPROC',
214 GetHookRecPointer name 'GETHOOKRECPOINTER',
215 StartKeyBoardHook name 'STARTKEYBOARDHOOK',
216 StopKeyBoardHook name 'STOPKEYBOARDHOOK';
217
218 begin
219 {Set our Dll's main entry point}
220 DLLProc := @DllEntryPoint;
221 {Call our Dll's main entry point}
222 DllEntryPoint(Dll_Process_Attach);
223
224 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:
225 unit TestHk1;
226
227 interface
228
229 uses
230 Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
231 StdCtrls,
232 ExtCtrls;
233
234 type
235 TForm1 = class(TForm)
236 Label1: TLabel;
237 Label2: TLabel;
238 Timer1: TTimer;
239 Button1: TButton;
240 procedure FormCreate(Sender: TObject);
241 procedure FormDestroy(Sender: TObject);
242 procedure Timer1Timer(Sender: TObject);
243 procedure Button1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
244 private
245 { Private declarations }
246 public
247 { Public declarations }
248 end;
249
250 var
251 Form1: TForm1;
252
253 implementation
254
255 {$R *.DFM}
256
257 {Functions prototypes for the hook dll}
258 type
259 TGetHookRecPointer = function: pointer stdcall;
260 type
261 TStartKeyBoardHook = procedure stdcall;
262 type
263 TStopKeyBoardHook = procedure stdcall;
264
265 {The record type filled in by the hook dll}
266 type
267 THookRec = packed record
268 TheHookHandle: HHOOK;
269 TheAppWinHandle: HWND;
270 TheCtrlWinHandle: HWND;
271 TheKeyCount: DWORD;
272 end;
273
274 {A pointer type to the hook record}
275 type
276 PHookRec = ^THookRec;
277
278 var
279 hHookLib: THandle; {A handle to the hook dll}
280 GetHookRecPointer: TGetHookRecPointer; {Function pointer}
281 StartKeyBoardHook: TStartKeyBoardHook; {Function pointer}
282 StopKeyBoardHook: TStopKeyBoardHook; {Function pointer}
283 LibLoadSuccess: bool; {If the hook lib was successfully loaded}
284 lpHookRec: PHookRec; {A pointer to the hook record}
285 EnterKeyCount: DWORD; {An internal count of the Enter Key}
286
287 procedure TForm1.FormCreate(Sender: TObject);
288 begin
289 {Set our initial variables}
290 Timer1.Enabled := FALSE;
291 Timer1.Interval := 1000;
292 Label1.Caption := '0 Keys Logged';
293 Label2.Caption := '0 Enter Keys Logged';
294 EnterKeyCount := 0;
295 lpHookRec := nil;
296 LibLoadSuccess := FALSE;
297 @GetHookRecPointer := nil;
298 @StartKeyBoardHook := nil;
299 @StopKeyBoardHook := nil;
300 {Try to load the hook dll}
301 hHookLib := LoadLibrary('THEHOOK.DLL');
302 {If the hook dll was loaded successfully}
303 if hHookLib <> 0 then
304 begin
305 {Get the function addresses}
306 @GetHookRecPointer := GetProcAddress(hHookLib, 'GETHOOKRECPOINTER');
307 @StartKeyBoardHook := GetProcAddress(hHookLib, 'STARTKEYBOARDHOOK');
308 @StopKeyBoardHook := GetProcAddress(hHookLib, 'STOPKEYBOARDHOOK');
309 {Did we find all the functions we need?}
310 if ((@GetHookRecPointer <> nil) and (@StartKeyBoardHook <> nil) and
311 (@StopKeyBoardHook <> nil)) then
312 begin
313 LibLoadSuccess := TRUE;
314 {Get a pointer to the hook record}
315 lpHookRec := GetHookRecPointer;
316 {Were we successfull in getting a ponter to the hook record}
317 if (lpHookRec <> nil) then
318 begin
319 {Fill in our portion of the hook record}
320 lpHookRec^.TheHookHandle := 0;
321 lpHookRec^.TheCtrlWinHandle := Button1.Handle;
322 lpHookRec^.TheKeyCount := 0;
323 {Start the keyboard hook}
324 StartKeyBoardHook;
325 {Start the timer if the hook was successfully set}
326 if (lpHookRec^.TheHookHandle <> 0) then
327 begin
328 Timer1.Enabled := TRUE;
329 end;
330 end;
331 end
332 else
333 begin
334 {We failed to find all the functions we need}
335 FreeLibrary(hHookLib);
336 hHookLib := 0;
337 @GetHookRecPointer := nil;
338 @StartKeyBoardHook := nil;
339 @StopKeyBoardHook := nil;
340 end;
341 end;
342 end;
343
344 procedure TForm1.FormDestroy(Sender: TObject);
345 begin
346 {Did we load the dll successfully?}
347 if (LibLoadSuccess = TRUE) then
348 begin
349 {Did we sucessfully get a pointer to the hook record?}
350 if (lpHookRec <> nil) then
351 begin
352 {Did the hook get set?}
353 if (lpHookRec^.TheHookHandle <> 0) then
354 begin
355 Timer1.Enabled := FALSE;
356 StopKeyBoardHook;
357 end;
358 end;
359 {Free the hook dll}
360 FreeLibrary(hHookLib);
361 end;
362 end;
363
364 procedure TForm1.Timer1Timer(Sender: TObject);
365 begin
366 {Display the number of keystrokes logged}
367 Label1.Caption := IntToStr(lpHookRec^.TheKeyCount) + ' Keys Logged';
368 end;
369
370 procedure TForm1.Button1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
371 begin
372 {Process message sent from hook dll and display number of time the enter key was
373 pressed}
374 if (Key = 0) then
375 begin
376 Inc(EnterKeyCount);
377 Label2.Caption := IntToStr(EnterKeyCount) + ' Enter Keys Logged';
378 end;
379 end;
380
381 end.
|