Skip to main content

memf_linux/
capabilities.rs

1//! Linux process capabilities analysis for privilege escalation detection.
2//!
3//! Linux capabilities split root privileges into granular units
4//! (CAP_SYS_ADMIN, CAP_NET_RAW, CAP_SYS_PTRACE, etc.). Each process has
5//! effective, permitted, and inheritable capability sets stored in
6//! `task_struct.cred->cap_effective/cap_permitted/cap_inheritable`.
7//!
8//! Processes with unusual capabilities -- especially non-root with elevated
9//! caps -- indicate privilege escalation and are flagged as suspicious.
10
11use memf_core::object_reader::ObjectReader;
12use memf_format::PhysicalMemoryProvider;
13
14use crate::{Error, ProcessInfo, Result};
15
16// ---------------------------------------------------------------------------
17// Capability bit constants (from include/uapi/linux/capability.h)
18// ---------------------------------------------------------------------------
19
20/// Override DAC access restrictions.
21const CAP_DAC_OVERRIDE: u64 = 1 << 1;
22/// Allow network administration (e.g., interface config, firewall rules).
23const CAP_NET_ADMIN: u64 = 1 << 12;
24/// Allow raw socket access (packet sniffing, crafting).
25const CAP_NET_RAW: u64 = 1 << 13;
26/// Allow loading/unloading kernel modules.
27const CAP_SYS_MODULE: u64 = 1 << 16;
28/// Allow ptrace of any process (process injection, debugging).
29const CAP_SYS_PTRACE: u64 = 1 << 19;
30/// Catch-all admin capability (mount, sethostname, reboot, etc.).
31const CAP_SYS_ADMIN: u64 = 1 << 21;
32
33/// Process capability information extracted from `task_struct.cred`.
34#[derive(Debug, Clone, serde::Serialize)]
35pub struct ProcessCapabilities {
36    /// Process ID.
37    pub pid: u64,
38    /// Process command name.
39    pub name: String,
40    /// Bitmask of effective capabilities.
41    pub effective: u64,
42    /// Bitmask of permitted capabilities.
43    pub permitted: u64,
44    /// Bitmask of inheritable capabilities.
45    pub inheritable: u64,
46    /// True if the process is non-root with elevated capabilities.
47    pub is_suspicious: bool,
48    /// Names of the suspicious capabilities held by a non-root process.
49    pub suspicious_caps: Vec<String>,
50}
51
52/// All known capabilities for name lookup.
53/// `(bit_value, name)` pairs used by [`cap_name`].
54const ALL_CAPS: &[(u64, &str)] = &[
55    (CAP_DAC_OVERRIDE, "CAP_DAC_OVERRIDE"),
56    (CAP_NET_ADMIN, "CAP_NET_ADMIN"),
57    (CAP_NET_RAW, "CAP_NET_RAW"),
58    (CAP_SYS_MODULE, "CAP_SYS_MODULE"),
59    (CAP_SYS_PTRACE, "CAP_SYS_PTRACE"),
60    (CAP_SYS_ADMIN, "CAP_SYS_ADMIN"),
61];
62
63/// Map a single capability bit to its human-readable name.
64///
65/// Returns `"UNKNOWN"` for unrecognized bits.
66pub fn cap_name(bit: u64) -> &'static str {
67    for &(cap_bit, name) in ALL_CAPS {
68        if bit == cap_bit {
69            return name;
70        }
71    }
72    "UNKNOWN"
73}
74
75/// Classify whether a process's effective capabilities are suspicious.
76///
77/// A process is suspicious if it is **non-root** (uid != 0) and holds any
78/// dangerous capability (e.g. `CAP_SYS_ADMIN`, `CAP_SYS_PTRACE`, `CAP_SYS_MODULE`, `CAP_NET_RAW`).
79///
80/// Returns `(is_suspicious, list_of_suspicious_cap_names)`.
81pub use crate::heuristics::classify_capabilities;
82
83/// Walk capability information for each process in the provided list.
84///
85/// For each process, reads `task_struct.cred` (a pointer to the `cred`
86/// struct), then reads `cap_effective`, `cap_permitted`, `cap_inheritable`
87/// (each a `kernel_cap_t`, typically a pair of u32s or a single u64
88/// depending on kernel version) and `uid` from the `cred` struct.
89///
90/// Applies [`classify_capabilities`] to flag privilege escalation.
91pub fn walk_capabilities<P: PhysicalMemoryProvider>(
92    reader: &ObjectReader<P>,
93    processes: &[ProcessInfo],
94) -> Result<Vec<ProcessCapabilities>> {
95    if processes.is_empty() {
96        return Ok(Vec::new());
97    }
98
99    let mut results = Vec::with_capacity(processes.len());
100
101    for proc in processes {
102        if let Ok(caps) = read_process_caps(reader, proc) {
103            results.push(caps);
104        }
105    }
106
107    Ok(results)
108}
109
110/// Read capability information from a single process's `task_struct.cred`.
111fn read_process_caps<P: PhysicalMemoryProvider>(
112    reader: &ObjectReader<P>,
113    proc: &ProcessInfo,
114) -> Result<ProcessCapabilities> {
115    // task_struct.cred -> pointer to cred struct
116    let cred_ptr: u64 = reader.read_field(proc.vaddr, "task_struct", "cred")?;
117    if cred_ptr == 0 {
118        return Err(Error::WalkFailed {
119            walker: "read_process_caps",
120            reason: "cred pointer is NULL".into(),
121        });
122    }
123
124    // cred.uid (kuid_t, effectively u32)
125    let uid: u32 = reader.read_field(cred_ptr, "cred", "uid")?;
126
127    // Read capability bitmasks from cred struct.
128    // kernel_cap_t is typically { u32 cap[_KERNEL_CAPABILITY_U32S] }.
129    // On 64-bit kernels with VFS caps v3 this is a single u64;
130    // on older kernels it may be two u32s. We read as u64 which covers
131    // both layouts when the field is declared as unsigned long.
132    let effective: u64 = reader.read_field(cred_ptr, "cred", "cap_effective")?;
133    let permitted: u64 = reader.read_field(cred_ptr, "cred", "cap_permitted")?;
134    let inheritable: u64 = reader.read_field(cred_ptr, "cred", "cap_inheritable")?;
135
136    let (is_suspicious, suspicious_caps) = classify_capabilities(effective, uid);
137
138    Ok(ProcessCapabilities {
139        pid: proc.pid,
140        name: proc.comm.clone(),
141        effective,
142        permitted,
143        inheritable,
144        is_suspicious,
145        suspicious_caps,
146    })
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use memf_core::object_reader::ObjectReader;
153    use memf_core::test_builders::{flags, PageTableBuilder};
154    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
155    use memf_symbols::isf::IsfResolver;
156    use memf_symbols::test_builders::IsfBuilder;
157
158    /// Helper: create an ObjectReader from ISF and page table builders.
159    fn make_reader(
160        isf: &IsfBuilder,
161        builder: PageTableBuilder,
162    ) -> ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
163        let json = isf.build_json();
164        let resolver = IsfResolver::from_value(&json).unwrap();
165        let (cr3, mem) = builder.build();
166        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
167        ObjectReader::new(vas, Box::new(resolver))
168    }
169
170    /// Helper: build a minimal ProcessInfo for testing.
171    fn fake_process(pid: u64, comm: &str, vaddr: u64) -> ProcessInfo {
172        ProcessInfo {
173            pid,
174            ppid: 1,
175            comm: comm.to_string(),
176            state: crate::types::ProcessState::Running,
177            vaddr,
178            cr3: None,
179            start_time: 0,
180        }
181    }
182
183    #[test]
184    fn cap_name_known() {
185        assert_eq!(cap_name(CAP_SYS_ADMIN), "CAP_SYS_ADMIN");
186        assert_eq!(cap_name(CAP_SYS_PTRACE), "CAP_SYS_PTRACE");
187        assert_eq!(cap_name(CAP_NET_RAW), "CAP_NET_RAW");
188        assert_eq!(cap_name(CAP_NET_ADMIN), "CAP_NET_ADMIN");
189        assert_eq!(cap_name(CAP_SYS_MODULE), "CAP_SYS_MODULE");
190        assert_eq!(cap_name(CAP_DAC_OVERRIDE), "CAP_DAC_OVERRIDE");
191    }
192
193    #[test]
194    fn cap_name_unknown() {
195        // A bit that doesn't match any known capability.
196        assert_eq!(cap_name(1 << 30), "UNKNOWN");
197    }
198
199    #[test]
200    fn classify_root_not_suspicious() {
201        // Root (uid=0) with all caps set should NOT be flagged.
202        let (suspicious, caps) = classify_capabilities(u64::MAX, 0);
203        assert!(!suspicious, "root should never be flagged as suspicious");
204        assert!(caps.is_empty(), "root should have no suspicious cap names");
205    }
206
207    #[test]
208    fn classify_nonroot_elevated_suspicious() {
209        // Non-root (uid=1000) with CAP_SYS_ADMIN should be flagged.
210        let effective = CAP_SYS_ADMIN | CAP_NET_RAW;
211        let (suspicious, caps) = classify_capabilities(effective, 1000);
212        assert!(
213            suspicious,
214            "non-root with CAP_SYS_ADMIN should be suspicious"
215        );
216        assert!(caps.contains(&"CAP_SYS_ADMIN".to_string()));
217        assert!(caps.contains(&"CAP_NET_RAW".to_string()));
218    }
219
220    #[test]
221    fn classify_nonroot_normal_benign() {
222        // Non-root (uid=1000) with no special caps should NOT be flagged.
223        let effective = CAP_DAC_OVERRIDE | CAP_NET_ADMIN;
224        let (suspicious, caps) = classify_capabilities(effective, 1000);
225        assert!(
226            !suspicious,
227            "non-root without critical caps should not be suspicious"
228        );
229        assert!(caps.is_empty());
230    }
231
232    #[test]
233    fn walk_capabilities_empty() {
234        // Empty process list should return empty Vec.
235        let isf = IsfBuilder::new();
236        let ptb = PageTableBuilder::new();
237        let reader = make_reader(&isf, ptb);
238
239        let result = walk_capabilities(&reader, &[]).unwrap();
240        assert!(
241            result.is_empty(),
242            "expected empty vec for empty process list"
243        );
244    }
245
246    #[test]
247    fn walk_capabilities_reads_cred() {
248        // Integration test: set up a synthetic task_struct -> cred -> caps.
249        let task_vaddr: u64 = 0xFFFF_8000_0010_0000;
250        let task_paddr: u64 = 0x0080_0000;
251        let cred_vaddr: u64 = 0xFFFF_8000_0020_0000;
252        let cred_paddr: u64 = 0x0090_0000;
253
254        // Offsets within task_struct
255        let cred_offset: u64 = 1608; // task_struct.cred
256
257        // Offsets within cred struct
258        let uid_offset: u64 = 4; // cred.uid
259        let cap_effective_offset: u64 = 40; // cred.cap_effective
260        let cap_permitted_offset: u64 = 48; // cred.cap_permitted
261        let cap_inheritable_offset: u64 = 56; // cred.cap_inheritable
262
263        let isf = IsfBuilder::new()
264            .add_struct("task_struct", 9024)
265            .add_field("task_struct", "cred", cred_offset, "pointer")
266            .add_struct("cred", 176)
267            .add_field("cred", "uid", uid_offset, "unsigned int")
268            .add_field(
269                "cred",
270                "cap_effective",
271                cap_effective_offset,
272                "unsigned long",
273            )
274            .add_field(
275                "cred",
276                "cap_permitted",
277                cap_permitted_offset,
278                "unsigned long",
279            )
280            .add_field(
281                "cred",
282                "cap_inheritable",
283                cap_inheritable_offset,
284                "unsigned long",
285            );
286
287        // uid=1000 (non-root), effective has CAP_SYS_ADMIN
288        let effective_caps: u64 = CAP_SYS_ADMIN | CAP_DAC_OVERRIDE;
289        let permitted_caps: u64 = CAP_SYS_ADMIN | CAP_DAC_OVERRIDE | CAP_NET_RAW;
290        let inheritable_caps: u64 = 0;
291
292        let ptb = PageTableBuilder::new()
293            .map_4k(task_vaddr, task_paddr, flags::WRITABLE)
294            .map_4k(cred_vaddr, cred_paddr, flags::WRITABLE)
295            // Write cred pointer in task_struct
296            .write_phys_u64(task_paddr + cred_offset, cred_vaddr)
297            // Write uid in cred
298            .write_phys_u64(cred_paddr + uid_offset, 1000u64)
299            // Write capability bitmasks in cred
300            .write_phys_u64(cred_paddr + cap_effective_offset, effective_caps)
301            .write_phys_u64(cred_paddr + cap_permitted_offset, permitted_caps)
302            .write_phys_u64(cred_paddr + cap_inheritable_offset, inheritable_caps);
303
304        let reader = make_reader(&isf, ptb);
305        let procs = vec![fake_process(42, "evil_proc", task_vaddr)];
306
307        let result = walk_capabilities(&reader, &procs).unwrap();
308        assert_eq!(result.len(), 1);
309
310        let cap = &result[0];
311        assert_eq!(cap.pid, 42);
312        assert_eq!(cap.name, "evil_proc");
313        assert_eq!(cap.effective, effective_caps);
314        assert_eq!(cap.permitted, permitted_caps);
315        assert_eq!(cap.inheritable, inheritable_caps);
316        assert!(
317            cap.is_suspicious,
318            "non-root with CAP_SYS_ADMIN should be suspicious"
319        );
320        assert!(cap.suspicious_caps.contains(&"CAP_SYS_ADMIN".to_string()));
321    }
322
323    #[test]
324    fn walk_capabilities_root_not_flagged() {
325        // Root process with all caps should not be flagged.
326        let task_vaddr: u64 = 0xFFFF_8000_0010_0000;
327        let task_paddr: u64 = 0x0080_0000;
328        let cred_vaddr: u64 = 0xFFFF_8000_0020_0000;
329        let cred_paddr: u64 = 0x0090_0000;
330
331        let cred_offset: u64 = 1608;
332        let uid_offset: u64 = 4;
333        let cap_effective_offset: u64 = 40;
334        let cap_permitted_offset: u64 = 48;
335        let cap_inheritable_offset: u64 = 56;
336
337        let isf = IsfBuilder::new()
338            .add_struct("task_struct", 9024)
339            .add_field("task_struct", "cred", cred_offset, "pointer")
340            .add_struct("cred", 176)
341            .add_field("cred", "uid", uid_offset, "unsigned int")
342            .add_field(
343                "cred",
344                "cap_effective",
345                cap_effective_offset,
346                "unsigned long",
347            )
348            .add_field(
349                "cred",
350                "cap_permitted",
351                cap_permitted_offset,
352                "unsigned long",
353            )
354            .add_field(
355                "cred",
356                "cap_inheritable",
357                cap_inheritable_offset,
358                "unsigned long",
359            );
360
361        let ptb = PageTableBuilder::new()
362            .map_4k(task_vaddr, task_paddr, flags::WRITABLE)
363            .map_4k(cred_vaddr, cred_paddr, flags::WRITABLE)
364            .write_phys_u64(task_paddr + cred_offset, cred_vaddr)
365            // uid=0 (root)
366            .write_phys_u64(cred_paddr + uid_offset, 0u64)
367            .write_phys_u64(cred_paddr + cap_effective_offset, u64::MAX)
368            .write_phys_u64(cred_paddr + cap_permitted_offset, u64::MAX)
369            .write_phys_u64(cred_paddr + cap_inheritable_offset, 0u64);
370
371        let reader = make_reader(&isf, ptb);
372        let procs = vec![fake_process(1, "init", task_vaddr)];
373
374        let result = walk_capabilities(&reader, &procs).unwrap();
375        assert_eq!(result.len(), 1);
376        assert!(
377            !result[0].is_suspicious,
378            "root process should not be flagged"
379        );
380        assert!(result[0].suspicious_caps.is_empty());
381    }
382}