diff --git a/Cargo.lock b/Cargo.lock index 2f7f35f4..85728252 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,9 +21,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] @@ -292,9 +292,11 @@ dependencies = [ "bpf-builder", "bpf-common", "cgroups-rs", + "lazy_static", "log", "nix 0.26.2", "pulsar-core", + "regex", "thiserror", "tokio", "which", @@ -1156,7 +1158,7 @@ dependencies = [ "lalrpop-util", "petgraph", "regex", - "regex-syntax", + "regex-syntax 0.6.28", "string_cache", "term", "tiny-keccak", @@ -1280,9 +1282,9 @@ checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memoffset" @@ -1772,13 +1774,25 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.1" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-automata", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-automata" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", ] [[package]] @@ -1787,6 +1801,12 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + [[package]] name = "reqwest" version = "0.11.20" diff --git a/crates/bpf-filtering/Cargo.toml b/crates/bpf-filtering/Cargo.toml index 53fd78bc..94835658 100644 --- a/crates/bpf-filtering/Cargo.toml +++ b/crates/bpf-filtering/Cargo.toml @@ -23,6 +23,8 @@ bpf-common = { path = "../bpf-common" } pulsar-core = { path = "../pulsar-core" } which = { version = "4.2.5", optional = true } cgroups-rs = { version = "0.3.2", optional = true } +regex = "1.9" +lazy_static = "1.4" [build-dependencies] bpf-builder = { path = "../bpf-builder" } diff --git a/crates/bpf-filtering/src/initializer.rs b/crates/bpf-filtering/src/initializer.rs index ff8e6d3c..d60a5a8f 100644 --- a/crates/bpf-filtering/src/initializer.rs +++ b/crates/bpf-filtering/src/initializer.rs @@ -74,12 +74,14 @@ pub async fn setup_events_filter( ppid: process.parent, pid: process.pid, timestamp: Timestamp::from(0), + namespaces: process.namespaces, }); process_tracker.update(TrackerUpdate::Exec { pid: process.pid, image: process.image.to_string(), timestamp: Timestamp::from(0), argv: Vec::new(), + namespaces: process.namespaces, }); } diff --git a/crates/bpf-filtering/src/process_tree.rs b/crates/bpf-filtering/src/process_tree.rs index 9a989732..0a902ef2 100644 --- a/crates/bpf-filtering/src/process_tree.rs +++ b/crates/bpf-filtering/src/process_tree.rs @@ -1,11 +1,18 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fs, path::Path}; use bpf_common::{ parsing::procfs::{self, ProcfsError}, Pid, }; +use lazy_static::lazy_static; +use pulsar_core::event::Namespaces; +use regex::Regex; use thiserror::Error; +lazy_static! { + static ref NAMESPACE_RE: Regex = Regex::new(r"(\d+)").unwrap(); +} + /// ProcessTree contains information about all running processes pub(crate) struct ProcessTree { processes: Vec, @@ -16,6 +23,7 @@ pub(crate) struct ProcessData { pub(crate) pid: Pid, pub(crate) image: String, pub(crate) parent: Pid, + pub(crate) namespaces: Namespaces, } #[derive(Debug, Error)] @@ -24,16 +32,70 @@ pub(crate) enum Error { ProcessNotFound { pid: Pid }, #[error("loading process {pid}: parent image {ppid} not found")] ParentNotFound { pid: Pid, ppid: Pid }, + #[error(transparent)] + Procfs(#[from] ProcfsError), + #[error("failed to get the {ns_type} namespace for process {pid}")] + Namespace { pid: Pid, ns_type: String }, } pub(crate) const PID_0: Pid = Pid::from_raw(0); +fn get_process_namespace(pid: Pid, ns_type: &str) -> Result { + let path = Path::new("/proc") + .join(pid.to_string()) + .join("ns") + .join(ns_type); + + let link_target = fs::read_link(path).map_err(|_| Error::Namespace { + pid, + ns_type: ns_type.to_owned(), + })?; + let link_target = link_target.to_string_lossy(); + let ns: u32 = NAMESPACE_RE + .captures(&link_target) + .and_then(|cap| cap.get(1)) + .and_then(|m| m.as_str().parse().ok()) + .ok_or(Error::Namespace { + pid, + ns_type: ns_type.to_owned(), + })?; + + Ok(ns) +} + +fn get_process_namespace_or_log(pid: Pid, namespace_type: &str) -> u32 { + get_process_namespace(pid, namespace_type).map_or_else( + |e| { + log::warn!( + "Failed to determine {} namespace for process {:?}: {}", + namespace_type, + pid, + e + ); + u32::default() + }, + |v| v, + ) +} + +fn get_process_namespaces(pid: Pid) -> Namespaces { + Namespaces { + uts: get_process_namespace_or_log(pid, "uts"), + ipc: get_process_namespace_or_log(pid, "ipc"), + mnt: get_process_namespace_or_log(pid, "mnt"), + net: get_process_namespace_or_log(pid, "net"), + pid: get_process_namespace_or_log(pid, "pid"), + time: get_process_namespace_or_log(pid, "time"), + cgroup: get_process_namespace_or_log(pid, "cgroup"), + } +} + impl ProcessTree { /// Construct the `ProcessTree` by reading from `procfs`: /// - process list /// - parent pid /// - image - pub(crate) fn load_from_procfs() -> Result { + pub(crate) fn load_from_procfs() -> Result { let mut processes: HashMap = HashMap::new(); let mut children: HashMap> = HashMap::new(); let mut sorted_processes: Vec = Vec::new(); @@ -50,18 +112,29 @@ impl ProcessTree { log::debug!("Error getting parent pid of {pid}: {}", err); Pid::from_raw(1) }); - processes.insert(pid, ProcessData { pid, image, parent }); + let namespaces = get_process_namespaces(pid); + processes.insert( + pid, + ProcessData { + pid, + image, + parent, + namespaces, + }, + ); children.entry(parent).or_default().push(pid); } // Make sure to add PID 0 (which is part of kernel) to map_interest to avoid // warnings about missing entries. + let namespaces = get_process_namespaces(PID_0); processes.insert( PID_0, ProcessData { pid: PID_0, image: String::from("kernel"), parent: PID_0, + namespaces, }, ); @@ -97,10 +170,12 @@ impl ProcessTree { match parent { Some(parent) => { let image = parent.image.to_string(); + let namespaces = get_process_namespaces(pid); self.processes.push(ProcessData { pid, image, parent: ppid, + namespaces, }); Ok(self.processes.last().unwrap()) } diff --git a/crates/modules/process-monitor/probes.bpf.c b/crates/modules/process-monitor/probes.bpf.c index 80dd3b1b..83ddb638 100644 --- a/crates/modules/process-monitor/probes.bpf.c +++ b/crates/modules/process-monitor/probes.bpf.c @@ -25,14 +25,26 @@ char LICENSE[] SEC("license") = "Dual BSD/GPL"; #define MAX_PENDING_DEAD_PARENTS 30 +struct namespaces { + unsigned int uts; + unsigned int ipc; + unsigned int mnt; + unsigned int pid; + unsigned int net; + unsigned int time; + unsigned int cgroup; +}; + struct fork_event { pid_t ppid; + struct namespaces namespaces; }; struct exec_event { struct buffer_index filename; int argc; struct buffer_index argv; + struct namespaces namespaces; }; struct exit_event { @@ -108,6 +120,13 @@ int BPF_PROG(sched_process_fork, struct task_struct *parent, return 0; event->fork.ppid = parent_tgid; + event->fork.namespaces.uts = BPF_CORE_READ(child, nsproxy, uts_ns, ns.inum); + event->fork.namespaces.ipc = BPF_CORE_READ(child, nsproxy, ipc_ns, ns.inum); + event->fork.namespaces.mnt = BPF_CORE_READ(child, nsproxy, mnt_ns, ns.inum); + event->fork.namespaces.pid = BPF_CORE_READ(child, nsproxy, pid_ns_for_children, ns.inum); + event->fork.namespaces.net = BPF_CORE_READ(child, nsproxy, net_ns, ns.inum); + event->fork.namespaces.time = BPF_CORE_READ(child, nsproxy, time_ns, ns.inum); + event->fork.namespaces.cgroup = BPF_CORE_READ(child, nsproxy, cgroup_ns, ns.inum); output_process_event(ctx, event); return 0; @@ -122,6 +141,15 @@ int BPF_PROG(sched_process_exec, struct task_struct *p, pid_t old_pid, if (!event) return 0; event->exec.argc = BPF_CORE_READ(bprm, argc); + + event->exec.namespaces.uts = BPF_CORE_READ(p, nsproxy, uts_ns, ns.inum); + event->exec.namespaces.ipc = BPF_CORE_READ(p, nsproxy, ipc_ns, ns.inum); + event->exec.namespaces.mnt = BPF_CORE_READ(p, nsproxy, mnt_ns, ns.inum); + event->exec.namespaces.pid = BPF_CORE_READ(p, nsproxy, pid_ns_for_children, ns.inum); + event->exec.namespaces.net = BPF_CORE_READ(p, nsproxy, net_ns, ns.inum); + event->exec.namespaces.time = BPF_CORE_READ(p, nsproxy, time_ns, ns.inum); + event->exec.namespaces.cgroup = BPF_CORE_READ(p, nsproxy, cgroup_ns, ns.inum); + // This is needed because the first MAX_IMAGE_LEN bytes of buffer will // be used as a lookup key for the target and whitelist maps and garbage // would make the search fail. diff --git a/crates/modules/process-monitor/src/lib.rs b/crates/modules/process-monitor/src/lib.rs index 63da93c0..425d2ab5 100644 --- a/crates/modules/process-monitor/src/lib.rs +++ b/crates/modules/process-monitor/src/lib.rs @@ -3,6 +3,7 @@ use bpf_common::{ ebpf_program, parsing::BufferIndex, program::BpfContext, BpfSender, Pid, Program, ProgramBuilder, ProgramError, }; +use pulsar_core::event::Namespaces; const MODULE_NAME: &str = "process-monitor"; @@ -35,11 +36,13 @@ pub async fn program( pub enum ProcessEvent { Fork { ppid: Pid, + namespaces: Namespaces, }, Exec { filename: BufferIndex, argc: u32, argv: BufferIndex, // 0 separated strings + namespaces: Namespaces, }, Exit { exit_code: u32, @@ -108,15 +111,17 @@ pub mod pulsar { // We do this by wrapping the pulsar sender and calling this closure on every event. BpfSenderWrapper::new(ctx.get_sender(), move |event: &BpfEvent| { let _ = tx_processes.send(match event.payload { - ProcessEvent::Fork { ppid } => TrackerUpdate::Fork { + ProcessEvent::Fork { ppid, namespaces } => TrackerUpdate::Fork { pid: event.pid, ppid, timestamp: event.timestamp, + namespaces, }, ProcessEvent::Exec { ref filename, argc, ref argv, + namespaces, } => { let argv = extract_parameters(argv.bytes(&event.buffer).unwrap_or_else(|err| { @@ -137,6 +142,7 @@ pub mod pulsar { image: filename.string(&event.buffer).unwrap_or_default(), timestamp: event.timestamp, argv, + namespaces, } } ProcessEvent::Exit { .. } => TrackerUpdate::Exit { @@ -180,17 +186,20 @@ pub mod pulsar { payload, buffer, .. } = event; Ok(match payload { - ProcessEvent::Fork { ppid } => Payload::Fork { + ProcessEvent::Fork { ppid, namespaces } => Payload::Fork { ppid: ppid.as_raw(), + namespaces, }, ProcessEvent::Exec { filename, argc, argv, + namespaces, } => Payload::Exec { filename: filename.string(&buffer)?, argc: argc as usize, argv: extract_parameters(argv.bytes(&buffer)?).into(), + namespaces, }, ProcessEvent::Exit { exit_code } => Payload::Exit { exit_code }, ProcessEvent::ChangeParent { ppid } => Payload::ChangeParent { diff --git a/crates/pulsar-core/src/event.rs b/crates/pulsar-core/src/event.rs index d49cd1e6..4da43147 100644 --- a/crates/pulsar-core/src/event.rs +++ b/crates/pulsar-core/src/event.rs @@ -158,11 +158,13 @@ pub enum Payload { }, Fork { ppid: i32, + namespaces: Namespaces, }, Exec { filename: String, argc: usize, argv: Argv, + namespaces: Namespaces, }, Exit { exit_code: u32, @@ -249,8 +251,8 @@ impl fmt::Display for Payload { Payload::FileLink { source, destination, hard_link } => write!(f,"File Link {{ source: {source}, destination: {destination}, hard_link: {hard_link} }}"), Payload::FileRename { source, destination } => write!(f,"File Rename {{ source: {source}, destination {destination} }}"), Payload::ElfOpened { filename, flags } => write!(f,"Elf Opened {{ filename: {filename}, flags: {flags} }}"), - Payload::Fork { ppid } => write!(f,"Fork {{ ppid: {ppid} }}"), - Payload::Exec { filename, argc, argv } => write!(f,"Exec {{ filename: {filename}, argc: {argc}, argv: {argv} }}"), + Payload::Fork { ppid, namespaces } => write!(f,"Fork {{ ppid: {ppid}, namespaces: {namespaces} }}"), + Payload::Exec { filename, argc, argv, namespaces } => write!(f,"Exec {{ filename: {filename}, argc: {argc}, argv: {argv}, namespaces: {namespaces} }}"), Payload::Exit { exit_code } => write!(f,"Exit {{ exit_code: {exit_code} }}"), Payload::ChangeParent { ppid } => write!(f,"Parent changed {{ ppid: {ppid} }}"), Payload::CgroupCreated { cgroup_path, cgroup_id } => write!(f,"Cgroup created {{ cgroup_path: {cgroup_path}, cgroup_id: {cgroup_id} }}"), @@ -478,6 +480,28 @@ impl fmt::Display for Argv { } } +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Validatron)] +pub struct Namespaces { + pub uts: u32, + pub ipc: u32, + pub mnt: u32, + pub pid: u32, + pub net: u32, + pub time: u32, + pub cgroup: u32, +} + +impl fmt::Display for Namespaces { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{{ uts: {}, ipc: {}, mnt: {}, pid: {}, net: {}, time: {}, cgroup: {} }}", + self.uts, self.ipc, self.mnt, self.pid, self.net, self.time, self.cgroup + ) + } +} + fn print_vec(f: &mut fmt::Formatter<'_>, v: impl IntoIterator) -> fmt::Result { write!(f, "[ ")?; diff --git a/crates/pulsar-core/src/pdk/module.rs b/crates/pulsar-core/src/pdk/module.rs index d7ba6404..bba2bbd7 100644 --- a/crates/pulsar-core/src/pdk/module.rs +++ b/crates/pulsar-core/src/pdk/module.rs @@ -243,6 +243,7 @@ impl ModuleSender { ppid, fork_time, argv: _, + namespaces: _, }) => { header.image = image; header.parent_pid = ppid.as_raw(); diff --git a/crates/pulsar-core/src/pdk/process_tracker.rs b/crates/pulsar-core/src/pdk/process_tracker.rs index 65bf2d1f..e85093bc 100644 --- a/crates/pulsar-core/src/pdk/process_tracker.rs +++ b/crates/pulsar-core/src/pdk/process_tracker.rs @@ -7,6 +7,8 @@ use tokio::{ time, }; +use crate::event::Namespaces; + pub fn start_process_tracker() -> ProcessTrackerHandle { let (tx, rx) = mpsc::unbounded_channel(); let mut process_tracker = ProcessTracker::new(rx); @@ -31,12 +33,14 @@ pub enum TrackerUpdate { pid: Pid, timestamp: Timestamp, ppid: Pid, + namespaces: Namespaces, }, Exec { pid: Pid, timestamp: Timestamp, image: String, argv: Vec, + namespaces: Namespaces, }, SetNewParent { pid: Pid, @@ -76,6 +80,7 @@ pub struct ProcessInfo { pub ppid: Pid, pub fork_time: Timestamp, pub argv: Vec, + pub namespaces: Namespaces, } impl ProcessTrackerHandle { @@ -136,6 +141,7 @@ struct ProcessData { String, // new image name >, argv: Vec, + namespaces: Namespaces, } /// Cleanup timeout in nanoseconds. This is how long an exited process @@ -159,6 +165,7 @@ impl ProcessTracker { original_image: "kernel".to_string(), exec_changes: BTreeMap::new(), argv: Vec::new(), + namespaces: Namespaces::default(), }, ); Self { @@ -241,6 +248,7 @@ impl ProcessTracker { pid, timestamp, ppid, + namespaces, } => { self.data.insert( pid, @@ -255,6 +263,7 @@ impl ProcessTracker { .get(&ppid) .map(|parent| parent.argv.clone()) .unwrap_or_default(), + namespaces, }, ); if let Some(pending_updates) = self.pending_updates.remove(&pid) { @@ -268,6 +277,7 @@ impl ProcessTracker { timestamp, ref mut image, ref mut argv, + namespaces: _, } => { if let Some(p) = self.data.get_mut(&pid) { p.exec_changes.insert(timestamp, std::mem::take(image)); @@ -320,6 +330,7 @@ impl ProcessTracker { ppid: process.ppid, fork_time: process.fork_time, argv: process.argv.clone(), + namespaces: process.namespaces, }) } @@ -423,6 +434,16 @@ mod tests { const PID_1: Pid = Pid::from_raw(42); const PID_2: Pid = Pid::from_raw(43); + const NAMESPACES_1: Namespaces = Namespaces { + uts: 4026531835, + ipc: 4026531839, + mnt: 4026531841, + pid: 4026531836, + net: 4026531840, + time: 4026531834, + cgroup: 4026531838, + }; + #[tokio::test] async fn no_processes_by_default() { let process_tracker = start_process_tracker(); @@ -443,12 +464,14 @@ mod tests { ppid: PID_1, pid: PID_2, timestamp: 10.into(), + namespaces: NAMESPACES_1, }); process_tracker.update(TrackerUpdate::Exec { pid: PID_2, image: "/bin/after_exec".to_string(), timestamp: 15.into(), argv: Vec::new(), + namespaces: NAMESPACES_1, }); process_tracker.update(TrackerUpdate::Exit { pid: PID_2, @@ -466,6 +489,7 @@ mod tests { ppid: PID_1, fork_time: 10.into(), argv: Vec::new(), + namespaces: NAMESPACES_1, } ); assert_eq!( @@ -475,6 +499,7 @@ mod tests { ppid: PID_1, fork_time: 10.into(), argv: Vec::new(), + namespaces: NAMESPACES_1, } ); assert_eq!( @@ -498,11 +523,13 @@ mod tests { image: "/bin/after_exec".to_string(), timestamp: 15.into(), argv: Vec::new(), + namespaces: NAMESPACES_1, }); process_tracker.update(TrackerUpdate::Fork { ppid: PID_1, pid: PID_2, timestamp: 10.into(), + namespaces: NAMESPACES_1, }); assert_eq!( process_tracker.get(PID_2, 9.into()).await, @@ -515,6 +542,7 @@ mod tests { ppid: PID_1, fork_time: 10.into(), argv: Vec::new(), + namespaces: NAMESPACES_1, }) ); assert_eq!( @@ -524,6 +552,7 @@ mod tests { ppid: PID_1, fork_time: 10.into(), argv: Vec::new(), + namespaces: NAMESPACES_1, }) ); time::sleep(time::Duration::from_millis(1)).await;