use memf_core::object_reader::ObjectReader;
use memf_format::PhysicalMemoryProvider;
use crate::{Error, ProcessInfo, Result};
const CAP_DAC_OVERRIDE: u64 = 1 << 1;
const CAP_NET_ADMIN: u64 = 1 << 12;
const CAP_NET_RAW: u64 = 1 << 13;
const CAP_SYS_MODULE: u64 = 1 << 16;
const CAP_SYS_PTRACE: u64 = 1 << 19;
const CAP_SYS_ADMIN: u64 = 1 << 21;
#[derive(Debug, Clone, serde::Serialize)]
pub struct ProcessCapabilities {
pub pid: u64,
pub name: String,
pub effective: u64,
pub permitted: u64,
pub inheritable: u64,
pub is_suspicious: bool,
pub suspicious_caps: Vec<String>,
}
const ALL_CAPS: &[(u64, &str)] = &[
(CAP_DAC_OVERRIDE, "CAP_DAC_OVERRIDE"),
(CAP_NET_ADMIN, "CAP_NET_ADMIN"),
(CAP_NET_RAW, "CAP_NET_RAW"),
(CAP_SYS_MODULE, "CAP_SYS_MODULE"),
(CAP_SYS_PTRACE, "CAP_SYS_PTRACE"),
(CAP_SYS_ADMIN, "CAP_SYS_ADMIN"),
];
pub fn cap_name(bit: u64) -> &'static str {
for &(cap_bit, name) in ALL_CAPS {
if bit == cap_bit {
return name;
}
}
"UNKNOWN"
}
pub use crate::heuristics::classify_capabilities;
pub fn walk_capabilities<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
processes: &[ProcessInfo],
) -> Result<Vec<ProcessCapabilities>> {
if processes.is_empty() {
return Ok(Vec::new());
}
let mut results = Vec::with_capacity(processes.len());
for proc in processes {
if let Ok(caps) = read_process_caps(reader, proc) {
results.push(caps);
}
}
Ok(results)
}
fn read_process_caps<P: PhysicalMemoryProvider>(
reader: &ObjectReader<P>,
proc: &ProcessInfo,
) -> Result<ProcessCapabilities> {
let cred_ptr: u64 = reader.read_field(proc.vaddr, "task_struct", "cred")?;
if cred_ptr == 0 {
return Err(Error::WalkFailed {
walker: "read_process_caps",
reason: "cred pointer is NULL".into(),
});
}
let uid: u32 = reader.read_field(cred_ptr, "cred", "uid")?;
let effective: u64 = reader.read_field(cred_ptr, "cred", "cap_effective")?;
let permitted: u64 = reader.read_field(cred_ptr, "cred", "cap_permitted")?;
let inheritable: u64 = reader.read_field(cred_ptr, "cred", "cap_inheritable")?;
let (is_suspicious, suspicious_caps) = classify_capabilities(effective, uid);
Ok(ProcessCapabilities {
pid: proc.pid,
name: proc.comm.clone(),
effective,
permitted,
inheritable,
is_suspicious,
suspicious_caps,
})
}
#[cfg(test)]
mod tests {
use super::*;
use memf_core::object_reader::ObjectReader;
use memf_core::test_builders::{flags, PageTableBuilder};
use memf_core::vas::{TranslationMode, VirtualAddressSpace};
use memf_symbols::isf::IsfResolver;
use memf_symbols::test_builders::IsfBuilder;
fn make_reader(
isf: &IsfBuilder,
builder: PageTableBuilder,
) -> ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
let json = isf.build_json();
let resolver = IsfResolver::from_value(&json).unwrap();
let (cr3, mem) = builder.build();
let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
ObjectReader::new(vas, Box::new(resolver))
}
fn fake_process(pid: u64, comm: &str, vaddr: u64) -> ProcessInfo {
ProcessInfo {
pid,
ppid: 1,
comm: comm.to_string(),
state: crate::types::ProcessState::Running,
vaddr,
cr3: None,
start_time: 0,
}
}
#[test]
fn cap_name_known() {
assert_eq!(cap_name(CAP_SYS_ADMIN), "CAP_SYS_ADMIN");
assert_eq!(cap_name(CAP_SYS_PTRACE), "CAP_SYS_PTRACE");
assert_eq!(cap_name(CAP_NET_RAW), "CAP_NET_RAW");
assert_eq!(cap_name(CAP_NET_ADMIN), "CAP_NET_ADMIN");
assert_eq!(cap_name(CAP_SYS_MODULE), "CAP_SYS_MODULE");
assert_eq!(cap_name(CAP_DAC_OVERRIDE), "CAP_DAC_OVERRIDE");
}
#[test]
fn cap_name_unknown() {
assert_eq!(cap_name(1 << 30), "UNKNOWN");
}
#[test]
fn classify_root_not_suspicious() {
let (suspicious, caps) = classify_capabilities(u64::MAX, 0);
assert!(!suspicious, "root should never be flagged as suspicious");
assert!(caps.is_empty(), "root should have no suspicious cap names");
}
#[test]
fn classify_nonroot_elevated_suspicious() {
let effective = CAP_SYS_ADMIN | CAP_NET_RAW;
let (suspicious, caps) = classify_capabilities(effective, 1000);
assert!(
suspicious,
"non-root with CAP_SYS_ADMIN should be suspicious"
);
assert!(caps.contains(&"CAP_SYS_ADMIN".to_string()));
assert!(caps.contains(&"CAP_NET_RAW".to_string()));
}
#[test]
fn classify_nonroot_normal_benign() {
let effective = CAP_DAC_OVERRIDE | CAP_NET_ADMIN;
let (suspicious, caps) = classify_capabilities(effective, 1000);
assert!(
!suspicious,
"non-root without critical caps should not be suspicious"
);
assert!(caps.is_empty());
}
#[test]
fn walk_capabilities_empty() {
let isf = IsfBuilder::new();
let ptb = PageTableBuilder::new();
let reader = make_reader(&isf, ptb);
let result = walk_capabilities(&reader, &[]).unwrap();
assert!(
result.is_empty(),
"expected empty vec for empty process list"
);
}
#[test]
fn walk_capabilities_reads_cred() {
let task_vaddr: u64 = 0xFFFF_8000_0010_0000;
let task_paddr: u64 = 0x0080_0000;
let cred_vaddr: u64 = 0xFFFF_8000_0020_0000;
let cred_paddr: u64 = 0x0090_0000;
let cred_offset: u64 = 1608;
let uid_offset: u64 = 4; let cap_effective_offset: u64 = 40; let cap_permitted_offset: u64 = 48; let cap_inheritable_offset: u64 = 56;
let isf = IsfBuilder::new()
.add_struct("task_struct", 9024)
.add_field("task_struct", "cred", cred_offset, "pointer")
.add_struct("cred", 176)
.add_field("cred", "uid", uid_offset, "unsigned int")
.add_field(
"cred",
"cap_effective",
cap_effective_offset,
"unsigned long",
)
.add_field(
"cred",
"cap_permitted",
cap_permitted_offset,
"unsigned long",
)
.add_field(
"cred",
"cap_inheritable",
cap_inheritable_offset,
"unsigned long",
);
let effective_caps: u64 = CAP_SYS_ADMIN | CAP_DAC_OVERRIDE;
let permitted_caps: u64 = CAP_SYS_ADMIN | CAP_DAC_OVERRIDE | CAP_NET_RAW;
let inheritable_caps: u64 = 0;
let ptb = PageTableBuilder::new()
.map_4k(task_vaddr, task_paddr, flags::WRITABLE)
.map_4k(cred_vaddr, cred_paddr, flags::WRITABLE)
.write_phys_u64(task_paddr + cred_offset, cred_vaddr)
.write_phys_u64(cred_paddr + uid_offset, 1000u64)
.write_phys_u64(cred_paddr + cap_effective_offset, effective_caps)
.write_phys_u64(cred_paddr + cap_permitted_offset, permitted_caps)
.write_phys_u64(cred_paddr + cap_inheritable_offset, inheritable_caps);
let reader = make_reader(&isf, ptb);
let procs = vec![fake_process(42, "evil_proc", task_vaddr)];
let result = walk_capabilities(&reader, &procs).unwrap();
assert_eq!(result.len(), 1);
let cap = &result[0];
assert_eq!(cap.pid, 42);
assert_eq!(cap.name, "evil_proc");
assert_eq!(cap.effective, effective_caps);
assert_eq!(cap.permitted, permitted_caps);
assert_eq!(cap.inheritable, inheritable_caps);
assert!(
cap.is_suspicious,
"non-root with CAP_SYS_ADMIN should be suspicious"
);
assert!(cap.suspicious_caps.contains(&"CAP_SYS_ADMIN".to_string()));
}
#[test]
fn walk_capabilities_root_not_flagged() {
let task_vaddr: u64 = 0xFFFF_8000_0010_0000;
let task_paddr: u64 = 0x0080_0000;
let cred_vaddr: u64 = 0xFFFF_8000_0020_0000;
let cred_paddr: u64 = 0x0090_0000;
let cred_offset: u64 = 1608;
let uid_offset: u64 = 4;
let cap_effective_offset: u64 = 40;
let cap_permitted_offset: u64 = 48;
let cap_inheritable_offset: u64 = 56;
let isf = IsfBuilder::new()
.add_struct("task_struct", 9024)
.add_field("task_struct", "cred", cred_offset, "pointer")
.add_struct("cred", 176)
.add_field("cred", "uid", uid_offset, "unsigned int")
.add_field(
"cred",
"cap_effective",
cap_effective_offset,
"unsigned long",
)
.add_field(
"cred",
"cap_permitted",
cap_permitted_offset,
"unsigned long",
)
.add_field(
"cred",
"cap_inheritable",
cap_inheritable_offset,
"unsigned long",
);
let ptb = PageTableBuilder::new()
.map_4k(task_vaddr, task_paddr, flags::WRITABLE)
.map_4k(cred_vaddr, cred_paddr, flags::WRITABLE)
.write_phys_u64(task_paddr + cred_offset, cred_vaddr)
.write_phys_u64(cred_paddr + uid_offset, 0u64)
.write_phys_u64(cred_paddr + cap_effective_offset, u64::MAX)
.write_phys_u64(cred_paddr + cap_permitted_offset, u64::MAX)
.write_phys_u64(cred_paddr + cap_inheritable_offset, 0u64);
let reader = make_reader(&isf, ptb);
let procs = vec![fake_process(1, "init", task_vaddr)];
let result = walk_capabilities(&reader, &procs).unwrap();
assert_eq!(result.len(), 1);
assert!(
!result[0].is_suspicious,
"root process should not be flagged"
);
assert!(result[0].suspicious_caps.is_empty());
}
}