Windows Functions For Writing Key Logging Software

Some quick notes on functions used for logging inputs on Windows, both the mouse and keyboard. I’m going to assume you’re familiar with the Windows Platform SDK or something similar where you can include Windows.h and get access to HWNDs.

The classic message pump for a Windows class will let you listen to mouse and keyboard inputs for a particular window or application. We’re going to listen to all the keyboard and mouse messages system-wide across the OS.

Disclaimer: For educational purposes only, I’m not responsible for any accidents or abuse with this information – damages, trouble, etc.

Handles To Hook Processes

Two HHOOKs are created. These are handles to register system inputs being redirected to a callback. There are two because one is for the keyboard, and one is for the mouse.

// The handle to route keyboard input into the callback.
HHOOK keyHook = nullptr;
// The handle to route mouse input into the callback.
HHOOK mouseHook = nullptr;

Callbacks are of type HOOKPROC.

HOOKPROC Hookproc;

LRESULT Hookproc(
  int code,
  WPARAM wParam,
  LPARAM lParam
)
{...}

The HOOKPROC should not be confused with a WNDPROC. While they both have a WPARAM and LPARAM, they’re not used the same way.

Callbacks

Here are some examples of hook callbacks.

This one listens for keyboard events and writes them to a file.

LRESULT CALLBACK LowLevelKeyboardProc(
    _In_ int    nCode,
    _In_ WPARAM wParam,
    _In_ LPARAM lParam
)
{
    if (frame != nullptr && frame->IsRecording() == true)
    {
        
        KBDLLHOOKSTRUCT* pkhs = (KBDLLHOOKSTRUCT*)lParam;

        std::chrono::system_clock::time_point tp = std::chrono::system_clock::now();
        unsigned long milliseconds_since_epoch = tp.time_since_epoch() / std::chrono::milliseconds(1);

        if (wParam == WM_KEYDOWN)
            frame->outstream << "keydown " << milliseconds_since_epoch << " " << pkhs->vkCode << "\n";
        else if (wParam == WM_KEYUP)
            frame->outstream << "keyup " << milliseconds_since_epoch << " " << pkhs->vkCode << "\n";
        else if (wParam == WM_SYSKEYDOWN)
            frame->outstream << "sysdown " << milliseconds_since_epoch << " " << pkhs->vkCode << "\n";
        else if (wParam == WM_SYSKEYUP)
            frame->outstream << "sysup " << milliseconds_since_epoch << " " << pkhs->vkCode << "\n";
    }

    return  CallNextHookEx(0, nCode, wParam, lParam);
}

Note how for a keyboard callback, the lParam is a pointer to more information. Also, note how we get information differently than from a normal Windows callback (WNDPROC).
See KBDLLHOOKSTRUCT for more details.

And here’s the callback for the mouse:

void LogMouseButton(bool down, int button, unsigned long msTime, POINT pt)
{
    if (down == true)
        frame->outstream << "mdown  ";
    else 
        frame->outstream << "mup  ";
        
    frame->outstream  << (unsigned long) msTime << " " << button << " " <<  pt.x << " " << pt.y << "\n";
}

LRESULT CALLBACK LowLevelMouseProc(_In_ int    nCode, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
    if (frame != nullptr && frame->IsRecording() == true)
    {
        MSLLHOOKSTRUCT* pmhs = (MSLLHOOKSTRUCT*)lParam;

        std::chrono::system_clock::time_point tp = std::chrono::system_clock::now();
        unsigned long milliseconds_since_epoch = tp.time_since_epoch() / std::chrono::milliseconds(1);

        // Record the even to file
        if (wParam == WM_LBUTTONDOWN)
            LogMouseButton(true, 0, milliseconds_since_epoch, pmhs->pt);
        else if (wParam == WM_LBUTTONUP)
            LogMouseButton(false, 0, milliseconds_since_epoch, pmhs->pt);
        else if (wParam == WM_RBUTTONDOWN)
            LogMouseButton(true, 1, milliseconds_since_epoch, pmhs->pt);
        else if (wParam == WM_RBUTTONUP)
            LogMouseButton(false, 1, milliseconds_since_epoch, pmhs->pt);
        else if (wParam == WM_MBUTTONDOWN)
            LogMouseButton(true, 2, milliseconds_since_epoch, pmhs->pt);
        else if (wParam == WM_MBUTTONUP)
            LogMouseButton(false, 2, milliseconds_since_epoch, pmhs->pt);
        else if (wParam == WM_MOUSEMOVE)
            frame->outstream << "mmove " << milliseconds_since_epoch << " " << pmhs->pt.x << " " << pmhs->pt.y << "\n";
        else if (wParam == WM_MOUSEWHEEL)
            frame->outstream << "mwheel " << milliseconds_since_epoch << " " << (short)HIWORD(pmhs->mouseData) << "\n";
        else if (wParam == WM_MOUSEHWHEEL)
            frame->outstream << "mhwheel " << milliseconds_since_epoch << "\n";
    }

    return  CallNextHookEx(0, nCode, wParam, lParam);
}

It’s similar to the keyboard callback, only it uses a different struct for additional info, and we’re tracking different Windows messages.
See MSLLHOOKSTRUCT for more information.

Registering The Callbacks

So we have variables as handles for registering the callbacks and the callbacks. How do we hook them up? Well, when you’re ready to start listening to the computer’s input, call SetWindowsHookExA.

I’ve included the whole function I’m using, but the relevant function calls are at line 24 and line 26.

bool MyFrame::StartRecording(std::string path)
{
    if (this->IsRecording() == true && this->recordingPath == path)
        return false;

    if (this->IsRecording() == true)
        this->StopRecording();

    this->recordingPath = path;
    this->outstream.open(path);
    
    // Prevent time values from being output as hex
    outstream.precision(15);

    if (this->outstream.is_open() == true)
    {
        this->outstream << "start " << GetMilliTime() << "\n";
        this->DumpAppAnchor();
        
        // Light up the record button to show the user visual feedback that we're recording.
        this->SetRecordButton(true);

        // Start redirecting keyboard messages to the callback.
        this->keyHook = SetWindowsHookExA(WH_KEYBOARD_LL, LowLevelKeyboardProc, NULL, NULL);
        // Start redirecting mouse messages to the callback.
        this->mouseHook = SetWindowsHookExA(WH_MOUSE_LL, LowLevelMouseProc, NULL, NULL);

        // If we're recording, it's counter-intuitive to allow the user to change the recording path.
        this->savePath->Disable();
        return true;
    }

    wxMessageBox("Failed to start recording.", "Recording");

    this->SetRecordButton(false);
    return false;
}

And that’s basically it. Just make sure to call UnhookWindowsHookEx when you’re done to free resources and overhead. See lines 8 and 14 below.

bool MyFrame::StopRecording()
{
    this->outstream.close();
    this->SetRecordButton(false);

    if (this->keyHook != nullptr)
    {
        UnhookWindowsHookEx(this->keyHook);
        this->keyHook = nullptr;
    }

    if (this->mouseHook != nullptr)
    {
        UnhookWindowsHookEx(this->mouseHook);
        this->mouseHook = nullptr;
    }

    this->savePath->Enable();
    return true;
}

And that’s basically it!

Debugging Caveats

One comment on debugging. When you register these callbacks, you’re actually injecting yourself into the messaging chain. A message chain that other parts of the OS will be waiting on. When receiving these system messages, if you pause the application – say from a debugger like Visual Studio – and if you need a keyboard or mouse to resume the application…

Then you can see how there’s an issue. We’ve clogged the inputs by pausing our program, but we need to input to resume our program. And we’re not just clogging the input to our application, but the entire system. Luckily inputs will still kind of work – at least in my experience. But it’s sluggish and not ideal.

So be careful with breakpoints when hooked into the message chain.

Example Log

Here’s an example of a log it recorded.

start 603621905
mmove 603622392 460 112
mmove 603622397 460 112
mmove 603622400 460 113
mmove 603622401 460 113
mmove 603622403 460 113
mmove 603622407 459 114
mmove 603622410 459 115
...
mmove 603628838 1237 218
mmove 603628844 1237 218
mmove 603628846 1237 218
mmove 603628858 1238 218
mdown  603629392 0 1238 218
mup  603629493 0 1238 218       

I put in the ellipse because it’s a lot of mouse movements recorded in the middle. It’s triggered from a GUI when a press a button. The click at the end is me clicking the button again to stop the recording.

– Stay strong, code on. William Leu