- Compiling
- Running
- Looking for Keyboard Device
- Reading Keyboard Events
- Termination
- Daemonizing Process
- Single Instance Daemon and File Locking
- Server
- References
- Disclaimer
make
Synopsis:
./keylogger host port
- host: server hostname or ipv4 address
- port: server port
sudo ./keylogger <server-ip> 12345
Running example (on host machine):
./server
On Unix-based systems, devices are typically found in the /dev/input/
directory. The function int keyboardFound(char *path, int *keyboard_fd)
located in keylogger.c is responsible for iterating over all files of /dev/input/
and its subdirectories to locate the keyboard device. To minimize false positives, the function performs three checks on each device:
-
Check for keys support: The device must support keys, as keyboards are expected to have key functionality.
-
Check for no relative and absolute movement support: Merely checking if the device supports keys might not be sufficient, as other devices like gaming mices and joysticks also have keys.
-
Check for common keyboard keys support: Although the first two checks help filter potential keyboards, there may still be false positives. For example, the power button is a device that supports keys, doesn't have relative and absolute movement support, but has one key (the key to power on and off the host device). To conclusively verify that the device is indeed a keyboard, a function examines whether it supports some common keyboard keys. A selection of twelve commonly used keys, such as 'KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_BACKSPACE, KEY_ENTER, KEY_0, KEY_1, KEY_2, KEY_ESC', is used for this purpose.
To do so the event API (EVIOC* functions
) is used and will allow us to query the capabilities and characteristics of an input device. The following function uses the linux event API to check whether or not an input device support specific input events:
int hasEventTypes(int fd, unsigned long evbit_to_check)
{
unsigned long evbit = 0;
/* Get the bit field of available event types. */
ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);
/* Check if EV_* set in $evbit_to_check are set. */
return ((evbit & evbit_to_check) == evbit_to_check);
}
/* Returns true iff the given device supports keys. */
int hasKeys(int fd)
{
return hasEventTypes(fd, (1 << EV_KEY));
}
Function hasEventTypes
takes two inputs: the file descriptor of the device and a bitmask representing the event types to be checked. The ioctl
system call, using the EVIOCGBIT
macro, populates the evbit
bitmask, where each bit corresponds to a supported event type by the device. Subsequently, a bitwise AND operation is performed between the evbit
bitmask (containing all supported event types by the device) and the evbit_to_check
bitmask (representing the event types to be checked). If the result of this operation exactly matches the evbit_to_check
bitmask, it indicates that the device supports the specified event types.
As an example, let's illustrate the function's usage using a file descriptor for a gaming mouse. Suppose the gaming mouse has the EV_KEY
, EV_REL
, and EV_LED
event type bits set, and we want to check if the current device supports EV_KEY
and EV_REL
(i.e., it is a gaming mouse).
-
The hasEventTypes(fd, evbit_to_check) is called with fd being the descriptor of the gaming mouse and
evbit_to_check
is a bitmask representingEV_KEY
andEV_REL
, obtained by OR-ing the bitmasks associated with these two event types:evbit_to_check = (1 << EV_KEY) | (1 << EV_REL) = 0000...000010 | 0000...000100 = 0000...000110
. The left shift operator << moves bit 1 EV_X positions to the left, starting from 0. -
The
ioctl()
system call retrieves the supported event types and fills theevbit
bitmask. Only three bits will be high in this bitmask: the second bit for theEV_KEY
event type (asEV_KEY
has a constant value of 1), the third bit for theEV_REL
event type (asEV_REL
has a constant value of 2), and the eleventh bit for theEV_LED
event type (asEV_LED
has a constant value of 11). So, theevbit
bitmask in binary would be00000..100000000110
. -
Finally, we perform the bitwise AND operation between
evbit
andevbit_to_check
, resulting inevbit & evbit_to_check = 00000..100000000110 & 0000...000000000110 = 0000...000000000110 = evbit_to_check
. This indicates that the device supports key events and relative movement since bothEV_KEY
andEV_REL
bits are set in theevbit
bitmask.
To retrieve events from a device, you need to use the standard character device "read" function. Each time you read from an event device, you will receive a set of events. Each event is represented by a struct input_event
. If you want to learn more about the fields in the input_event structure, you can refer to the documentation at https://www.kernel.org/doc/Documentation/input/event-codes.txt.
struct input_event {
struct timeval time;
unsigned short type;
unsigned short code;
unsigned int value;
};
When attempting to read from the keyboard device, you might not receive exactly what you expect, which is a single event containing the code of the key you just pressed. This discrepancy occurs because, in general, a single hardware event is translated into multiple "software" input events. Let's consider what happens when I read from the keyboard device after pressing the letter "a":
As you can see, a single key press has generated six input events. Let's take a look at each of those events:
-
Type = 4: Indicates an EV_MSC event, which, according to the documentation, is used to describe miscellaneous input data that does not fit into other types. From my understanding, it returns the "value" field as the device-specific scan code. Therefore, we are not particularly interested in it, as it might yield wrong key codes if the user remaps the keys. However, it could be useful to recognize which specific physical buttons are being pressed.
-
Type = 1: This is the event we are mostly interested in. It corresponds to type = EV_KEY, indicating that a key has either been pressed, released, or repeated. A value of 1 tells us that a key has been pressed, and code = "30" represents the KEY_A key, which is indeed the key I pressed.
-
Type = 0: Indicates an EV_SYN event, which is simply used to separate different hardware events.
The other three events generated are almost the same as the first three, and they are associated with the hardware event of "releasing a key." For instance, if you take a look at the fifth event, you can see that we have an EV_KEY event with value = 0, representing a key release, in this case, of the letter "a".
In my program, only key press events will be captured, specifically events whose type = EV_KEY and value = 1.
Keylogger process can terminate gracefully by receiving two signals:
- SIGTERM
- SIGPIPE (Shutting down Server)
The keylogger will operate in the background without direct user control. To achieve this, I have implemented the program as a daemon process. The process is converted into a daemon using the int daemonize() function. I have included comments in the code that explain each step necessary for daemonizing a process.
The keylogger is designed to run as a single instance at any given time. To ensure this, the daemon creates a file named "keylogger-daemon.pid" and places a write lock on the entire file, also writing its own process ID (PID) into it. This mechanism prevents other processes from acquiring a write lock on the same file, resulting in failure, as another instance of the daemon already owns the lock. In other words, only one instance of the keylogger daemon can be running simultaneously. The following function checks whether another instance of the daemon is already running. If not, it returns the locked file.
It is a simple non-blocking single-threaded server which logs keypress events, to its standard output.
Read operations could potentially block indefinitely, especially when the client is not pressing any keys for an extended period. This behavior would cause our main thread to block, preventing it from accepting new clients and servicing other tasks. To address this issue, we take two key measures:-
We mark the sockets as non-blocking. By doing so, the read operations on these sockets will not wait indefinitely for data, and if no data is available, the read will return immediately with an error indicating that no data is present.
-
We utilize the
poll
system call, which allows the thread to be notified by the kernel whenever specific events occur, such as when the socket becomes readable (in our case). This enables efficient handling of multiple connections without blocking the main thread.
When using the poll
system call, once we have read all available data from the sockets, subsequent read attempts will not block but instead fail, and the errno
will be set to EWOULDBLOCK
or EAGAIN
, indicating that there is no data to read. This enables the main thread to regain control and continue servicing other tasks and accepting new clients as needed. In this way, we ensure that the keylogger remains responsive even during idle periods on the client side.
Let us look at an example of server receiving events from two clients:
- About system calls, signals, daemon processes and other C programming stuff: https://www.amazon.com/Advanced-Programming-UNIX-Environment-3rd/dp/0321637739
- About Linux input subsystem:
- About poll system call: https://www.ibm.com/docs/en/i/7.2?topic=designs-using-poll-instead-select
I have developed this program just to learn about the linux input subsystem and to put in practice notions I have acquired during the operating systems class. You shall not run this program on machines where you don't have permissions to log key presses!