Linux Kernel: How to Capture a Key Press and Replace It with Another Key

Linux Kernel: How to capture a key press and replace it with another key?

Consider next simple kernel module:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <asm/io.h>

#define KBD_IRQ 1 /* IRQ number for keyboard (i8042) */
#define KBD_DATA_REG 0x60 /* I/O port for keyboard data */
#define KBD_SCANCODE_MASK 0x7f
#define KBD_STATUS_MASK 0x80

static irqreturn_t kbd2_isr(int irq, void *dev_id)
{
char scancode;

scancode = inb(KBD_DATA_REG);
/* NOTE: i/o ops take a lot of time thus must be avoided in HW ISRs */
pr_info("Scan Code %x %s\n",
scancode & KBD_SCANCODE_MASK,
scancode & KBD_STATUS_MASK ? "Released" : "Pressed");

return IRQ_HANDLED;
}

static int __init kbd2_init(void)
{
return request_irq(KBD_IRQ, kbd2_isr, IRQF_SHARED, "kbd2", (void *)kbd2_isr);
}

static void __exit kbd2_exit(void)
{
free_irq(KBD_IRQ, (void *)kbd2_isr);
}

module_init(kbd2_init);
module_exit(kbd2_exit);

MODULE_LICENSE("GPL");

This is most minimal and primitive key-logger. It can be easily reworked for replacing of scan code.

Disclaimers

  • This module is not cross-platform (will work only on x86 architecture, because it's using inb() function)
  • I believe it only works with PS/2 keyboard (won't work with USB keyboard)
  • It's performing slow I/O operation (I mean pr_info()) in hardware IRQ handler, which should be avoided (ideally threaded IRQs should be used)).

But I think it's good for educational purposes -- it's really small and demonstrates the idea pretty well (without messing with API like input_dev, input_register_device(), serio_write(), input_event(), input_report_key(), etc).

Details

Real interrupt handler (in keyboard driver) requested as shared interrupt, which allows us also request that interrupt and thus handle it also in our ISR (in addition to ISR in original keyboard driver). Interrupt requesting is done in kbd2_init().

This module works as follows:

  1. Catches key press event (hardware interrupt handler kbd2_isr() is called for each key press event)
  2. Reads scan code of pressed key (via inb() function)
  3. And prints it via pr_info()

Now, you want to replace that scan code. I believe you can use outb() function for this (on x86). So I leave it for you.

If you wonder why we are requesting IRQ with number 1, see at drivers/input/serio/i8042-io.h:

#else
# define I8042_KBD_IRQ 1

Also be sure to check that this IRQ is shared in drivers/input/serio/i8042.c:

error = request_irq(I8042_KBD_IRQ, i8042_interrupt, IRQF_SHARED,
"i8042", i8042_platform_device);

Here is documentation for i8042 keyboard controller: AT keyboard controller.

Useful constants

To avoid magic numbers, you can use next definitions.

From drivers/input/serio/i8042-io.h:

/*
* Register numbers.
*/

#define I8042_COMMAND_REG 0x64
#define I8042_STATUS_REG 0x64
#define I8042_DATA_REG 0x60

From include/linux/i8042.h:

/*
* Status register bits.
*/

#define I8042_STR_PARITY 0x80
#define I8042_STR_TIMEOUT 0x40
#define I8042_STR_AUXDATA 0x20
#define I8042_STR_KEYLOCK 0x10
#define I8042_STR_CMDDAT 0x08
#define I8042_STR_MUXERR 0x04
#define I8042_STR_IBF 0x02
#define I8042_STR_OBF 0x01

Detecting keyboard key press and release on Linux

Your kbhit (and, apparently, even Windows' original kbhit) doesn't detect whether a key is pressed, but only whether there is something new to read on stdin. This will only be the case 25 times a second or so, depending on your autorepeat setting. Making stdout unbuffered in your example code will make that more obvious (000000W00000000W000000000W)

How can I fix this so that when a keyboard key is kept pressed, kbhit() always returns 0?

This is impossible to do portably. In Linux, it can be done using the device files under /dev/input
See the example program below. It will register all keys, even a lone SHIFT Note that this is effectively a keylogger, so you will have to run as root (or make the program setuid). It will then register all keystrokes, even when it doesn't have keyboard focus. Yikes!

Note: the example below is based on keystate.c by Kevin Cox

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sysexits.h>
#include <glob.h>
#include <linux/input.h>
#include <sys/stat.h>
#include <fcntl.h>

/* Return -1 if no key is being pressed, or else the lowest keycode
(c.f. linux/input-event-codes.h) of all the keys that are being pressed */
int keycode_of_key_being_pressed() {
FILE *kbd;
glob_t kbddev; // Glob structure for keyboard devices
glob("/dev/input/by-path/*-kbd", 0, 0, &kbddev); // Glob select all keyboards
int keycode = -1; // keycode of key being pressed
for (int i = 0; i < kbddev.gl_pathc ; i++ ) { // Loop through all the keyboard devices ...
if (!(kbd = fopen(kbddev.gl_pathv[i], "r"))) { // ... and open them in turn (slow!)
perror("Run as root to read keyboard devices");
exit(1);
}

char key_map[KEY_MAX/8 + 1]; // Create a bit array the size of the number of keys
memset(key_map, 0, sizeof(key_map)); // Fill keymap[] with zero's
ioctl(fileno(kbd), EVIOCGKEY(sizeof(key_map)), key_map); // Read keyboard state into keymap[]
for (int k = 0; k < KEY_MAX/8 + 1 && keycode < 0; k++) { // scan bytes in key_map[] from left to right
for (int j = 0; j <8 ; j++) { // scan each byte from lsb to msb
if (key_map[k] & (1 << j)) { // if this bit is set: key was being pressed
keycode = 8*k + j ; // calculate corresponding keycode
break; // don't scan for any other keys
}
}
}

fclose(kbd);
if (keycode)
break; // don't scan for any other keyboards
}
return keycode;
}

void main()
{
setvbuf(stdout, NULL, _IONBF, 0); // Set stdout unbuffered
while (1) {
int key = keycode_of_key_being_pressed();
printf((key < 0 ? "no key\n" : "keycode: %d\n"), key);
if (key == KEY_X)
exit(0);
}
}

How can I capture a key stroke immediately in linux?

Basically, it depends heavily on how you define immediately.

There are two tasks here. The first is to disable the regular key echoing that is built into most C input libraries. The second is to print out the new character instead of the old one.

In pseudo code.

 echo(off);
while (capturing && charIsAvailable()) {
c = readOneChar();
if (c == '\n') {
capturing = false;
}
printf("%c", c++);
}
echo(on);

There are a number of systems communicating to capture a key press.

  1. The keyboard
  2. (possibly) a USB bus
  3. The CPU interrupt handler
  4. The operating system
  5. The X window server process
  6. The X "window" that has focus.

The last step is done with a program that runs a continuous loop that captures events from the X server and processes them. If you wanted to expand this program in certain ways (get the length of time the key was pressed) you need to tell the other programs that you want "raw" keyboard events, which means that you won't really be receiving fully "cooked" characters. As a result, you will have to keep track of which keys are up and down, and how long, and handle all the odd meta key behavior in your program (that's not an 'a' it's a 'A' because shift is down, etc).

There are also other processing modes to consider, like canonical and non-canonical, which will control whether you wish the events to be received in line oriented chunks (line events) or character oriented chunks (character events). Again this is somewhat complicated by the need to make the upstream programs aware of the requirements of the downstream client.

Now that you have some idea of your environment, let's revisit the actual code needed to suppress character output.

// define a terminal configuration data structure
struct termios term;

// copy the stdin terminal configuration into term
tcgetattr( fileno(stdin), &term );

// turn off Canonical processing in term
term.c_lflag &= ~ICANON;

// turn off screen echo in term
term.c_lflag &= ~ECHO;

// set the terminal configuration for stdin according to term, now
tcsetattr( fileno(stdin), TCSANOW, &term);


(fetch characters here, use printf to show whatever you like)

// turn on Canonical processing in term
term.c_lflag |= ICANON;

// turn on screen echo in term
term.c_lflag |= ECHO;

// set the terminal configuration for stdin according to term, now
tcsetattr( fileno(stdin), TCSANOW, &term);

Even this is not immediate. To get immediate, you need to get closer to the source, which eventually means a kernel module (which still isn't as immediate as the keyboard micro-controller, which isn't as immediate as the moment the switch actually closes). With enough items in between the source and the destination, eventually it becomes possible to notice the difference, however, in practice this code has been worked on a lot by people who are seeking the best tradeoff between performance and flexibility.

How to detect all the key press events across any application and map certain sequences to commands?

I found the relevant details in a 2 part post on linuxjournal.com. The post details the input subsytem for linux kernel in a beginner friendly way.

  • https://www.linuxjournal.com/article/6396
  • https://www.linuxjournal.com/article/6429

Further, here is a link to source code of evtest. Credits for this link goes to Ruslan

Accessing Keys from Linux Input Device

Open the input device,

#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <linux/input.h>
#include <string.h>
#include <stdio.h>

static const char *const evval[3] = {
"RELEASED",
"PRESSED ",
"REPEATED"
};

int main(void)
{
const char *dev = "/dev/input/by-path/platform-i8042-serio-0-event-kbd";
struct input_event ev;
ssize_t n;
int fd;

fd = open(dev, O_RDONLY);
if (fd == -1) {
fprintf(stderr, "Cannot open %s: %s.\n", dev, strerror(errno));
return EXIT_FAILURE;
}

and then read keyboard events from the device:

    while (1) {
n = read(fd, &ev, sizeof ev);
if (n == (ssize_t)-1) {
if (errno == EINTR)
continue;
else
break;
} else
if (n != sizeof ev) {
errno = EIO;
break;
}

The above snippet breaks out from the loop if any error occurs, or if the userspace receives only a partial event structure (which should not happen, but might in some future/buggy kernels). You might wish to use a more robust read loop; I personally would be satisfied by replacing the last break with continue, so that partial event structures are ignored.

You can then examine the ev event structure to see what occurred, and finish the program:

        if (ev.type == EV_KEY && ev.value >= 0 && ev.value <= 2)
printf("%s 0x%04x (%d)\n", evval[ev.value], (int)ev.code, (int)ev.code);

}
fflush(stdout);
fprintf(stderr, "%s.\n", strerror(errno));
return EXIT_FAILURE;
}

For a keypress,

  • ev.time: time of the event (struct timeval type)

  • ev.type: EV_KEY

  • ev.code: KEY_*, key identifier; see complete list in /usr/include/linux/input.h

  • ev.value: 0 if key release, 1 if key press, 2 if autorepeat keypress

See Documentation/input/input.txt in the Linux kernel sources for further details.

The named constants in /usr/include/linux/input.h are quite stable, because it is a kernel-userspace interface, and the kernel developers try very hard to maintain compatibility. (That is, you can expect there to be new codes every now and then, but existing codes rarely change.)



Related Topics



Leave a reply



Submit