Detecting ransomware with eBPF in Rust
Title: Detecting ransomware with eBPF in Rust
Target: ~1000 words, B1-safe English, working-code-first
Build: demo + article for DEV.to / Draft.dev share
Source repo: github.com/BartoszOsiej/talus-process-monitor (MIT)
1. The idea (hook)
Ransomware works in a simple way: it opens your files, encrypts them, and writes them back. Fast. One process can open hundreds or thousands of files within seconds.
You do not need a huge model to spot it. You need to watch one number: how many files a single process opens per second.
This article shows a small eBPF program in Rust that does exactly that. It hooks two syscalls (execve and openat), counts file-open rate per process, and — when the rate is too high — kills the process.
All code comes from a real open-source project: talus-process-monitor (MIT, github.com/BartoszOsiej/talus-process-monitor).
2. What is eBPF in one sentence
eBPF lets you attach small programs to kernel events (syscalls, network packets, timers) without writing or loading a kernel module. The programs run in a sandboxed VM in the kernel. You get kernel-level visibility with low overhead.
In Rust, the library is called aya. aya-ebpf is the runtime for the kernel side. aya manages loading from userspace.
3. Kernel side: a tiny eBPF program
We define one "event" struct. Both kernel and userspace must agree on the layout, so it uses #[repr(C)]:
#[repr(C)]
pub struct ProcessEvent {
pub event_type: u8,
pub pid: u32,
pub uid: u32,
pub comm: [u8; 16], // process name
pub filename: [u8; 64], // opened file path
}
#[map]
pub static EVENTS: PerfEventArray<ProcessEvent> = PerfEventArray::new(0);
PerfEventArray is the channel between kernel and userspace. Each CPU has its own buffer, so concurrent processes do not block each other.
Next, two tracepoints. These run when a process starts (execve) or opens a file (openat):
#[tracepoint(name = "sys_enter_execve", category = "syscalls")]
pub fn sys_enter_execve(ctx: TracePointContext) -> u32 {
emit_event(&ctx, EVENT_EXECVE, 0)
}
#[tracepoint(name = "sys_enter_openat", category = "syscalls")]
pub fn sys_enter_openat(ctx: TracePointContext) -> u32 {
emit_event(&ctx, EVENT_OPENAT, 1)
}
The main work happens in emit_event. First we read PID and UID of the current process:
let pid = (bpf_get_current_pid_tgid() >> 32) as u32;
let uid = bpf_get_current_uid_gid() as u32;
Then we read the filename argument from the tracepoint args. The tracepoint layout is (common fields, filename, flags, mode), so the filename pointer sits at a fixed offset:
let ptr_size = core::mem::size_of::<*const c_char>();
let filename_offset = 16 + filename_arg as usize * ptr_size;
if let Ok(filename) = unsafe { ctx.read_at::<*const c_char>(filename_offset) } {
if !filename.is_null() {
// bpf_probe_read_user safely copies userspace memory
if let Ok(bytes) = unsafe {
bpf_probe_read_user_str_bytes(filename.cast::<u8>(), dst)
} { /* copy into event.filename */ }
}
}
Finally, we push the event into the buffer:
EVENTS.output(ctx, &event, 0);
One note about eBPF: you cannot use normal Rust std function calls. No memcpy, no memset, no format!. The code uses hand-written byte loops (raw_copy) to avoid LLVM builtins. This is a common eBPF gotcha:
unsafe fn raw_copy(dst: *mut u8, src: *const u8, len: usize) {
let mut i = 0;
while i < len {
*dst.add(i) = *src.add(i);
i += 1;
}
}
4. Userspace: the detection engine
On the userspace side we read events off the perf buffer and keep a 1-second sliding window of open events per PID:
self.windows.entry(ev.pid).or_default() // VecDeque<Instant>
let cutoff = now - Duration::from_secs(WINDOW_SECS); // 1s
while window.front().is_some_and(|t| *t < cutoff) {
window.pop_front();
}
window.push_back(now);
let opens_now = window.len();
If opens_now reaches the threshold (default 50 opens in 1 second), we fire an alert:
if self.threshold > 0 && stats.window_opens == self.threshold {
stats.alerts += 1;
outputs.push(Output::Alert(Alert { /* pid, comm, opens */ }));
if self.auto_kill {
let result = kill_process(ev.pid);
outputs.push(Output::Action(ResponseAction { /* ... */ }));
}
}
kill_process is a plain kill(2) with SIGKILL:
fn kill_process(pid: u32) -> bool {
let rc = unsafe { libc::kill(pid as i32, libc::SIGKILL) };
rc == 0
}
SIGKILL cannot be caught. The offending process stops immediately. That is the full "detect and respond" loop: hook → count → alert → kill.
5. Run it
Requirements: Linux kernel 5.8+, root (or CAP_BPF + CAP_SYS_ADMIN), Rust nightly and clang.
# Build
./build.sh
# Monitor only (no killing)
sudo target/release/process-monitor --alert-threshold 50
# EDR mode: detect and kill
sudo target/release/process-monitor --alert-threshold 50 --auto-kill
Test it with a loop that opens many files fast:
sudo process-monitor --alert-threshold 3 --auto-kill
# in another terminal:
for i in $(seq 1 100); do touch /tmp/f$i; done
The process that runs the loop violates the threshold and gets SIGKILL.
6. Limits and next steps
This heuristic has false positives (a backup tool also opens many files fast) — that is why a configurable threshold and a lower default matter. Real-world improvements from the same repo:
- Shannon entropy scoring on filenames: encrypted/randomized names have high entropy.
-
Network egress tracing (
connect,sendto): catch data exfiltration. -
File-extension tracking: mass
.enc/.lockedwrites.
The project also ships a small online-trained MLP model (no external deps) that turns raw event features into score: benign / suspicious / ransomware.
7. Summary
- eBPF gives you kernel-level tracing without kernel modules.
- Rust + aya makes the whole pipeline safe to build and maintain.
- A 1-second sliding window on
openatrate is a cheap, real ransomware signal. - Responding with SIGKILL turns a monitor into an EDR-style agent — 30 lines of Rust.
Full source code: github.com/BartoszOsiej/talus-process-monitor
Follow me on DEV.to for the next part: network egress detection with eBPF.
Try it on your machine
Talus is open source (MIT) — community edition includes the TUI, real-time eBPF tracing, and the ransomware heuristic.
If you want the full version — web dashboard, auto-kill response, SIEM exports (Kafka / ClickHouse / Memgraph) — Talus Enterprise is $50 one-time, perpetual: Get Talus Enterprise
Activation: talus license activate <KEY>