1use memf_core::object_reader::ObjectReader;
12use memf_format::PhysicalMemoryProvider;
13
14use crate::{Error, ProcessInfo, Result};
15
16const CAP_DAC_OVERRIDE: u64 = 1 << 1;
22const CAP_NET_ADMIN: u64 = 1 << 12;
24const CAP_NET_RAW: u64 = 1 << 13;
26const CAP_SYS_MODULE: u64 = 1 << 16;
28const CAP_SYS_PTRACE: u64 = 1 << 19;
30const CAP_SYS_ADMIN: u64 = 1 << 21;
32
33#[derive(Debug, Clone, serde::Serialize)]
35pub struct ProcessCapabilities {
36 pub pid: u64,
38 pub name: String,
40 pub effective: u64,
42 pub permitted: u64,
44 pub inheritable: u64,
46 pub is_suspicious: bool,
48 pub suspicious_caps: Vec<String>,
50}
51
52const 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
63pub 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
75pub use crate::heuristics::classify_capabilities;
82
83pub 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
110fn read_process_caps<P: PhysicalMemoryProvider>(
112 reader: &ObjectReader<P>,
113 proc: &ProcessInfo,
114) -> Result<ProcessCapabilities> {
115 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 let uid: u32 = reader.read_field(cred_ptr, "cred", "uid")?;
126
127 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 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 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 assert_eq!(cap_name(1 << 30), "UNKNOWN");
197 }
198
199 #[test]
200 fn classify_root_not_suspicious() {
201 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 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 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 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 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 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()
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 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_phys_u64(task_paddr + cred_offset, cred_vaddr)
297 .write_phys_u64(cred_paddr + uid_offset, 1000u64)
299 .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 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 .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}