Intro
Keyloggers on Linux are pretty rare - I guess, but they still exist. Are they easy to detect? Well… maybe, maybe not. It really depends on the adversary and how much effort this guy put to hide it. Anyway, I built this project because at some point I decided, “Let’s build a tool to detect them” - and here we are - Github link
Disclaimer: Before we start please do understand that this project is just an exploration and built out of curiosity.
Now, how the hell do you even find this goddamn keylogger anyway? Well, you can’t just scan or search for the word “keylogger” in running processes (you may find it if the adversary is that retarded) - real attackers aren’t that stupid I guess..
So, if we want to find them, we first have to understand how input is handled on Linux systems. On Linux, we have different ways to read input, and attackers can hook into any of them.
/dev/input/event* (evdev interface)
- This is the “normal” way. Every physical keyboard press is an event reported under
/dev/input/. - Legit apps like, for example, your desktop environment, X11, or Wayland compositor listen here.
/dev/hidraw* (raw HID interface)
- HID = Human Interface Device. These are the raw USB packets from keyboards/mice.
- If something grabs
/dev/hidraw0, it’s bypassing the higher-level event system and reading the raw keypresses.
evdev library / ioctl syscalls
- Instead of manually parsing
/dev/input/event*, a process can use the evdev API to subscribe to events. Under the hood, it’s the same source, but it makes detection trickier.
virtual input (uinput)
- There’s also
/dev/uinput, which isn’t really about reading input - it’s about creating fake devices. A keylogger could be using this in combo with something else, like: log keystrokes, then maybe replay them later as if they came from a legit keyboard.
higher layers (X11 / Wayland)
- On X11, a keylogger can just ask the server for global key events.
- On Wayland, I think its pretty strict to access inputs - unknown apps generally can’t grab global input (haven’t tested).
These are some of the ways that I could find to read inputs. The next step would be figuring out which process is using our/accessing input devices.
*Note: The below approaches described here are from the basis of how my tool detects suspicious activity.
Trying to find Keylogger’s
As a first step, I did was, to get the processes that have file descriptors
open. This alone could already let us flag any suspicious processes that have access to input. For deeper analysis, we could trace syscalls like read or ioctl using eBPF (which I will cover later in this blog).
Pseudocode: Detect processes accessing input devices(FD)
-
Get list of active input devices in
/dev/input/(those starting withevent*). -
For each process:
- Check if its
/proc/<pid>/fd/directory exists. - For each file descriptor of the process:
- Resolve the real path of the descriptor.
- If it matches an input device path:
- Record that this PID is using the device.
- Stop checking further descriptors for this process.
- Check if its
-
Return a mapping of:
-
{ pid - set of input devices it is accessing }
Example: { 1234: {/dev/input/event2}, 5678: {/dev/input/event3, /dev/input/event4} }
-
2) Detecting Processes Accessing Input via X11
Not all keyloggers open those files directly - some use higher-level APIs like X11 to capture keystrokes, so we should also get the processes who are connecting to X11 server and might be reading input events through it.
The logic I used is pretty simple:
- First we check a process’s
fdand see if it’s connected to the X11 Unix socket (/tmp/.X11-unix/X0). - Check its environment variables (
XAUTHORITY,DISPLAY) - if they exist, the process may likely have access to X11. - Check if the process has loaded X11- related libraries like
libX11.soorlibXt.so. - Combine all these signals into a confidence score. The more the signals, the higher the chance that the process is using X11 for input.
Pseudocode: Detect processes accessing input via X11
-
For each process:
- Collect open file descriptors.
- Check if any are sockets connected to
/tmp/.X11-unix/X0. - If yes, increase confidence score and note the number of sockets.
-
Check environment variables:
- If
XAUTHORITYexists - increase confidence. - If
DISPLAYexists - increase confidence.
- If
-
Check loaded libraries:
- If process has loaded
libX11.soorlibXt.so→ increase confidence.
- If process has loaded
-
Return:
- (confidence, access_rate)
Example: Process 405 - confidence=3, access_rate=1
- (confidence, access_rate)
3) eBPF Approach
Another way to detect keyloggers is using eBPF
. Instead of scanning /proc, we can attach eBPF programs directly in the kernel to monitor access to input devices - for example, syscalls like open/openat (to see who is trying to open /dev/input/event*) or kernel functions like vfs_read/vfs_ioctl (to see who is actually reading keystrokes).”
This way, whenever a process tries to access /dev/input/event*, we can capture that information in real time - which makes it more reliable and accurate.
eBPF Hooks That I Used
-
vfs_read → is triggered when a process reads from
/dev/input/event*.- This is the most direct sign that a process is capturing keystrokes.
- It also catches reads from
/dev/hidraw*(like USB keyboards), but sincevfs_readis used for any file read, it can get noisy. In practice, we should it by path (e.g., only/dev/input/*and/dev/hidraw*) which I did through - template_gen.py, see here .
-
vfs_ioctl → is triggered when a process uses
ioctlon an input device.- Some keyloggers rely on ioctl calls to fetch input data.
-
vfs_write → is triggered when a process writes to
/dev/uinput.- This is useful for detecting processes that try to inject fake keystrokes, making them appear as if they came from a legitimate device.
Other Approaches
Below are some other ways we could find processes that access input devices:
-
Static Module Analysis : By checking which libraries or modules a process has loaded, we can flag unusual behavior. For example, say a simple text editor, it shouldn’t normally load modules like
pyxhookor other input-capturing libraries. If it does, that’s suspicious. -
Suspicious String Scanning : Scanning binaries or scripts for strings like
keyboard,input_event,eventor code related to input hooks can expose hidden intent. -
Module Checking : Different tools (memory maps,
lsof,pmap, etc.) can reveal different sets of loaded modules used by programs. Say there’s a python program that is obfuscated and we can’t read the content of it and we want to know what libraries/modules it is using, to determine the intent of the program, in that cases, we could use this approach.
Now that we have some base and are able to find processes that access input devices, we have the most important part - Differentiate between a legitimate process accessing input and the one which is suspicious.
For example, legit processes like Xorg, browsers, or text editors must open /dev/input/event* to read the input, contrast to that a process that is running from a /tmp/ directory with no GUI accessing your input is much more suspicious, so…
The Strategy: How We Separate Legitimate and Suspicious Processes
For this, I have gone through multiple levels of checks; some are pretty basic, and some are very specific.
These checks will be done on processes that have input access.
Why so many checks?
Because no single check is bulletproof. If we want to catch keyloggers, one need to think like someone actually trying to hide one - not just flag every text editor or Xorg instance that accesses /dev/input/.
Note: That doesn’t mean that we will be not having ANY false positives, I tried to tackle this using the Trust/Untrust binary option, see
--help
-
Check where the binary lives - system folder or /tmp?
- Legit processes usually live under /usr/bin, /bin, /usr/libexec, etc.
- Anything in /tmp, /dev/shm, or a random home directory path is little sus.
- Attackers usually hide payloads in writable places that don’t need root access.
-
Who owns it? And who can write to it?
- Root-owned, unwritable binaries are usually safe.
- World-writable binaries/scripts are a big red flag - because they can be swapped out easily.
- A binary “owned” by some odd user account (like www-data or a throwaway UID) is also worth checking.
-
Is it registered with the package manager?
- pacman -Qo / dpkg -S / rpm -qf can tell you if a binary belongs to a legit package.
- No package record = maybe someone just dropped it there or the user downloaded it.
-
Is the binary packed or obfuscated (UPX, high entropy, magic headers)?
- Normal system binaries usually don’t need packing.
- UPX headers, compressed payloads, or random high-entropy blobs can be a hint that it is attempting to dodge static analysis.
-
Did the attacker try to run the deleted binary from memory - so it doesn’t show up in normal file searches?
- You’ll see this as path (deleted) in
/proc/<pid>/exe. - This is a classic trick to hide persistence: run once from /tmp, delete it, and let it stay memory-resident.
- This shows the intent to hide from forensics.
- You’ll see this as path (deleted) in
-
What launched the process - was it cron, atd, sshd, or something normal, like your desktop?
- If it’s hooked under cron, it might auto-respawn.
- Spawning under sshd could mean a remote attacker dropped it.
- Unexpected parent processes (bash, python, perl) starting daemons need to be checked.
-
Is it doing network stuff in the background like trying to send keystrokes somewhere else?
- Look for suspicious outbound connections (netstat, ss, /proc/net/*).
-
Are there signs of persistence that shouldn’t be there - cron jobs, autostart, weird shell profile entries?
- Check ~/.bashrc, ~/.zshrc, /etc/profile.d/.
- Look for odd systemd service units or ~/.config/autostart entries.
-
Is the process slinging around suspicious IPC artifacts, loading weird modules, or relying on LD_PRELOAD hacks in places that we never expect?
- Check /proc//maps and lsof for unusual shared objects.
- Malicious processes that may inject themselves via LD_PRELOAD or custom .so files.
- Check for unexpected named pipes, sockets, or /tmp/.X11-unix/ shenanigans.
Okay, now that we differentiated legit and not so legit processes accessing input, we just have to show the output to the user.
Displaying the findings
Default option – Initial System Checks (RECOMMENDED)
When running the tool without any extra flags, it will go through a set of basic checks that can help to find potential keylogger activity or persistence tricks. These include:
-
Find suspicious input devices – Keyloggers often create virtual devices or masquerade as generic HID (Human Interface Device) devices. If you see an extra “keyboard-like” device that you don’t own, then it could be a strong sign that it is a hidden monitoring software.
-
Inspect shell configuration (rc files) – Files like .bashrc, .zshrc, or .profile etc.. will run automatically every time a shell is opened. Adversaries could sneak in malicious commands here, so that the keylogger reactivates on every login.
-
Analyze PAM modules – PAM (Pluggable Authentication Modules) control Linux authentication (logins, sudo, ssh, etc.). A module that is compromised or is unknown, could be logging credentials directly during authentication. Verifying these modules ensures us that is no plugin that is silently harvesting your passwords.
-
Scan for suspicious aliases – Since, linux allows aliasing commands, attackers could exploit this and then silently run keyloggers or log your keystrokes whenever the commands are executed.
-
Check .inputrc files – This file defines how readline (library that is used for bash input) processes the keystrokes. Malicious entries could be used to log or redirect your typed commands or whatever you type bro.
-
Review cron jobs & scheduled tasks – Keyloggers often rely on cron jobs to relaunch themselves mostly after reboots or maybe at some random intervals. For example, a cron job might silently restart a hidden logging process. Inspecting the jobs could help us catch this.
-
Check for LD_PRELOAD abuse – LD_PRELOAD can force processes to load a custom library before others. Attackers can abuse this by injecting logging functions into every program that reads input. It’s a powerful and stealthy way to keylog across the system.
And other three main approaches:
Single PID Mode (--p)
- Takes a single PID from the user.
- Performs all checks (mentioned before) and these:
- Detects input device usage.
- Tracks file paths and parent processes.
- Performs file authenticity and obfuscation checks.
- Monitors persistence, network, and IPC/module indicators.
- Prints detailed findings for that process.
Quick Scan Mode (--scan)
- Scans all running processes.
- For each process, performs the same checks as Single PID Mode.
- Aggregates results and reports all suspicious findings at the end.
Monitor Mode (--monitor)
- Similar to Quick Scan but runs continuously until the user exits.
- Checks processes at a defined interval (
nseconds). - Provides real-time updates on input access and suspicious activity.
Some Other Things
Active User Input Prompt
During Single PID or Quick Scan monitoring, the tool will prompt the user to type a random sequence on the keyboard (it doesn’t matter whether you typed it wrong or right, we just need you to use your keyboard).
- Why?
Some keyloggers only activate when real keyboard input occurs. By generating actual keystrokes, we could increase the likelihood of triggering a keylogger and capturing its activity during the monitoring.
Trust / Untrust Binary Option (--modify_trust)
This option allows the user to manually mark a binary as trusted or untrusted.
- Why?
- The detection as I explained involves heuristics and can sometimes produce false positives. By letting the user specify which binaries are safe, the tool can avoid repeatedly flagging known safe processes and focus on genuinely suspicious activity.
- The
--alloption would skips this and scans all processes.
Conclusion
Remember, no tool can guarantee 100% detection. That’s it, that’s the conclusion. Sankyou