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.txtcd 03-malware-analysis/keyloggers/detection python3 keylogger_detector.py --help
Source
#!/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())