Skip to content
Threats Guide

keylogger_detector.py

Keylogger indicator scanner (Linux)

  • detection
  • python
  • 112 lines
  • may need admin rights

Run security tools only on systems you own or have written permission to test.

Run it

From the repository root, after pip install -r tools/requirements.txt
cd 03-malware-analysis/keyloggers/detection
python3 keylogger_detector.py --help

Source

View on GitHub
#!/usr/bin/env python3"""Keylogger indicator scanner (Linux)Location: 03-malware-analysis/keyloggers/detection/keylogger_detector.pyDEFENSIVE tool: it looks for the traces a keylogger leaves on a Linux machine and never captureskeystrokes itself.  INPUT_DEVICE   a process holds a keyboard device (/dev/input/event*) open that is not on the                 allow-list of normal input stacks (Xorg, Wayland compositors, libinput, ...)  LD_PRELOAD     a process was started with LD_PRELOAD, or /etc/ld.so.preload is non-empty                 (userland hooks can intercept library calls such as read())  KNOWN_TOOL     a running process looks like a known keylogger (logkeys, lkl, pykeylogger ...)  SCRIPT_HOOK    an interpreter command line references input-hooking modules (pynput, keyboard, evdev)Every finding is an INDICATOR: remote-desktop tools, accessibility software and gaming overlaysalso read input devices. Investigate before acting. Run as root to see other users' processes.Windows and macOS need different tooling (see README).Exit codes: 0 = no indicators, 1 = indicators found, 2 = unsupported platform.""" import argparseimport globimport jsonimport osimport reimport sys ALLOWED = {"xorg", "xwayland", "gnome-shell", "kwin_wayland", "kwin_x11", "mutter", "weston", "sway", "systemd-logind",           "libinput", "upowerd", "gsd-power", "gsd-media-keys", "ibus-daemon", "fcitx", "fcitx5", "sddm", "gdm", "lightdm",           "plasmashell", "kglobalaccel5", "kglobalacceld", "cosmic-comp", "hyprland"}KNOWN_TOOLS = re.compile(r"\b(logkeys|lkl|uberkey|pykeylogger|keysniffer|xkeylog|skeylogger|keylogger)\b", re.I)HOOK_MODULES = re.compile(r"\b(pynput|evdev|python-xlib|keyboard\.on_press|pyxhook)\b", re.I)SYSTEM_LIB_DIRS = ("/lib/", "/lib64/", "/usr/lib/", "/usr/lib64/", "/usr/local/lib/")  def read(path, binary=False):    try:        with open(path, "rb") as handle:            data = handle.read()        return data if binary else data.decode(errors="replace")    except OSError:        return ""  def scan(input_glob="/dev/input/event*", proc_root="/proc", preload_file="/etc/ld.so.preload", allowed=ALLOWED):    findings = []    devices = {os.path.realpath(p) for p in glob.glob(input_glob)}    me = os.getpid()    for pid in filter(str.isdigit, os.listdir(proc_root)):        if int(pid) == me:            continue        base = os.path.join(proc_root, pid)        name = read(os.path.join(base, "comm")).strip()        cmdline = read(os.path.join(base, "cmdline"), binary=True).replace(b"\0", b" ").decode(errors="replace").strip()         try:            for fd in os.listdir(os.path.join(base, "fd")):                target = os.path.realpath(os.path.join(base, "fd", fd))                if target in devices and name.lower() not in allowed:                    findings.append({"type": "INPUT_DEVICE", "severity": "HIGH", "pid": int(pid), "process": name,                                     "detail": f"holds input device {target} open"})                    break        except OSError:            pass         for entry in read(os.path.join(base, "environ")).split("\0"):            if entry.startswith("LD_PRELOAD="):                library = entry.split("=", 1)[1]                outside = not library.startswith(SYSTEM_LIB_DIRS)                findings.append({"type": "LD_PRELOAD", "severity": "HIGH" if outside else "MEDIUM", "pid": int(pid),                                 "process": name, "detail": f"LD_PRELOAD={library}" + (" (outside system library dirs)" if outside else "")})         argv = cmdline.split()        candidates = {name, *(os.path.basename(a) for a in argv[:2])}      # program and script name only        if any(KNOWN_TOOLS.search(c) for c in candidates):            findings.append({"type": "KNOWN_TOOL", "severity": "HIGH", "pid": int(pid), "process": name,                             "detail": cmdline[:120]})        elif name.lower().startswith(("python", "perl", "ruby", "node")) and HOOK_MODULES.search(cmdline):            findings.append({"type": "SCRIPT_HOOK", "severity": "LOW", "pid": int(pid), "process": name,                             "detail": cmdline[:120]})     preload = [l.strip() for l in read(preload_file).splitlines() if l.strip() and not l.startswith("#")]    if preload:        findings.append({"type": "LD_PRELOAD", "severity": "HIGH", "pid": 0, "process": "-",                         "detail": f"{preload_file} is not empty: {', '.join(preload)}"})    return findings  def main():    parser = argparse.ArgumentParser(description="Keylogger indicator scanner (Linux)")    parser.add_argument("-o", "--output")    parser.add_argument("--input-glob", default="/dev/input/event*", help=argparse.SUPPRESS)    args = parser.parse_args()    if not sys.platform.startswith("linux"):        print("[x] This scanner supports Linux only. See the README for Windows and macOS guidance.")        return 2    if os.geteuid() != 0:        print("[i] Not running as root: processes owned by other users cannot be inspected.")    findings = scan(args.input_glob)    for f in findings:        print(f"  [{f['severity']}] {f['type']}: pid {f['pid']} ({f['process']}) {f['detail']}")    print("[+] No keylogger indicators found." if not findings else f"[!] {len(findings)} indicator(s): investigate before acting")    if args.output:        json.dump(findings, open(args.output, "w"), indent=2)    return 1 if [f for f in findings if f["severity"] != "LOW"] else 0  if __name__ == "__main__":    sys.exit(main())