1use memf_core::object_reader::ObjectReader;
12use memf_format::PhysicalMemoryProvider;
13
14use crate::{ProcessInfo, Result};
15
16#[derive(Debug, Clone, serde::Serialize)]
18pub struct PtraceRelationship {
19 pub tracer_pid: u32,
21 pub tracer_name: String,
23 pub tracee_pid: u32,
25 pub tracee_name: String,
27 pub is_suspicious: bool,
29}
30
31pub use crate::heuristics::classify_ptrace;
33
34pub fn scan_ptrace_relationships<P: PhysicalMemoryProvider>(
36 reader: &ObjectReader<P>,
37 processes: &[ProcessInfo],
38) -> Result<Vec<PtraceRelationship>> {
39 if processes.is_empty() {
40 return Ok(Vec::new());
41 }
42
43 let mut results = Vec::new();
44
45 for proc in processes {
46 if let Ok(Some(rel)) = read_ptrace_info(reader, proc) {
47 results.push(rel)
48 }
49 }
50
51 Ok(results)
52}
53
54fn read_ptrace_info<P: PhysicalMemoryProvider>(
55 reader: &ObjectReader<P>,
56 proc: &ProcessInfo,
57) -> Result<Option<PtraceRelationship>> {
58 let ptrace_flags: u32 = reader.read_field(proc.vaddr, "task_struct", "ptrace")?;
59 if ptrace_flags == 0 {
60 return Ok(None);
61 }
62
63 let parent_ptr: u64 = reader.read_pointer(proc.vaddr, "task_struct", "parent")?;
64 let real_parent_ptr: u64 = reader.read_pointer(proc.vaddr, "task_struct", "real_parent")?;
65
66 if parent_ptr == real_parent_ptr || parent_ptr == 0 {
67 return Ok(None);
68 }
69
70 let tracer_pid: u32 = reader.read_field::<u32>(parent_ptr, "task_struct", "pid")?;
71 let tracer_name = reader.read_field_string(parent_ptr, "task_struct", "comm", 16)?;
72
73 let tracee_name = proc.comm.clone();
74 let is_suspicious = classify_ptrace(&tracer_name, &tracee_name);
75
76 Ok(Some(PtraceRelationship {
77 tracer_pid,
78 tracer_name,
79 tracee_pid: proc.pid as u32,
80 tracee_name,
81 is_suspicious,
82 }))
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use memf_core::object_reader::ObjectReader;
89 use memf_core::test_builders::PageTableBuilder;
90 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
91 use memf_symbols::isf::IsfResolver;
92 use memf_symbols::test_builders::IsfBuilder;
93
94 fn make_reader(
95 isf: &IsfBuilder,
96 builder: PageTableBuilder,
97 ) -> ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
98 let json = isf.build_json();
99 let resolver = IsfResolver::from_value(&json).unwrap();
100 let (cr3, mem) = builder.build();
101 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
102 ObjectReader::new(vas, Box::new(resolver))
103 }
104
105 #[allow(dead_code)]
106 fn fake_process(pid: u64, comm: &str, vaddr: u64) -> ProcessInfo {
107 ProcessInfo {
108 pid,
109 ppid: 1,
110 comm: comm.to_string(),
111 state: crate::types::ProcessState::Running,
112 vaddr,
113 cr3: None,
114 start_time: 0,
115 }
116 }
117
118 #[test]
119 fn classify_gdb_tracing_anything_is_benign() {
120 assert!(!classify_ptrace("gdb", "target_app"));
121 }
122
123 #[test]
124 fn classify_strace_tracing_bash_is_benign() {
125 assert!(!classify_ptrace("strace", "bash"));
126 }
127
128 #[test]
129 fn classify_unknown_tracing_sshd_is_suspicious() {
130 assert!(classify_ptrace("evil_inject", "sshd"));
131 }
132
133 #[test]
134 fn classify_unknown_tracing_passwd_is_suspicious() {
135 assert!(classify_ptrace("malware", "passwd"));
136 }
137
138 #[test]
139 fn classify_self_tracing_by_non_debugger_is_suspicious() {
140 assert!(classify_ptrace("sneaky", "sneaky"));
141 }
142
143 #[test]
144 fn classify_empty_tracer_name_is_suspicious() {
145 assert!(classify_ptrace("", "victim"));
146 }
147
148 #[test]
149 fn classify_normal_process_tracing_normal_process_is_benign() {
150 assert!(!classify_ptrace("my_app", "helper_proc"));
151 }
152
153 #[test]
154 fn scan_ptrace_empty_processes_returns_empty_vec() {
155 let isf = IsfBuilder::new();
156 let ptb = PageTableBuilder::new();
157 let reader = make_reader(&isf, ptb);
158
159 let result = scan_ptrace_relationships(&reader, &[]).unwrap();
160 assert!(result.is_empty());
161 }
162
163 #[test]
164 fn scan_ptrace_unreadable_task_struct_skips_process() {
165 let isf = IsfBuilder::new()
166 .add_struct("task_struct", 256)
167 .add_field("task_struct", "pid", 0, "int")
168 .add_field("task_struct", "ptrace", 8, "unsigned int")
169 .add_field("task_struct", "parent", 16, "pointer")
170 .add_field("task_struct", "real_parent", 24, "pointer")
171 .add_field("task_struct", "comm", 32, "char");
172 let ptb = PageTableBuilder::new();
173 let reader = make_reader(&isf, ptb);
174
175 let proc = fake_process(100, "bash", 0xDEAD_0000_0000_0000);
176 let result = scan_ptrace_relationships(&reader, &[proc]).unwrap();
177 assert!(result.is_empty());
178 }
179
180 #[test]
181 fn scan_ptrace_zero_ptrace_flags_skips_process() {
182 use memf_core::test_builders::flags as ptf;
183
184 let task_vaddr: u64 = 0xFFFF_8000_0010_0000;
185 let task_paddr: u64 = 0x0080_0000;
186
187 let mut data = vec![0u8; 512];
188 data[0..4].copy_from_slice(&200u32.to_le_bytes());
189 data[8..12].copy_from_slice(&0u32.to_le_bytes());
190
191 let isf = IsfBuilder::new()
192 .add_struct("task_struct", 256)
193 .add_field("task_struct", "pid", 0, "int")
194 .add_field("task_struct", "ptrace", 8, "unsigned int")
195 .add_field("task_struct", "parent", 16, "pointer")
196 .add_field("task_struct", "real_parent", 24, "pointer")
197 .add_field("task_struct", "comm", 32, "char");
198 let ptb = PageTableBuilder::new()
199 .map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
200 .write_phys(task_paddr, &data);
201 let reader = make_reader(&isf, ptb);
202
203 let proc = fake_process(200, "bash", task_vaddr);
204 let result = scan_ptrace_relationships(&reader, &[proc]).unwrap();
205 assert!(result.is_empty());
206 }
207
208 #[test]
209 fn classify_ptrace_lldb_is_benign() {
210 assert!(!classify_ptrace("lldb", "target"));
211 }
212
213 #[test]
214 fn classify_ptrace_ltrace_is_benign() {
215 assert!(!classify_ptrace("ltrace", "any"));
216 }
217
218 #[test]
219 fn classify_ptrace_valgrind_is_benign() {
220 assert!(!classify_ptrace("valgrind", "leaky"));
221 }
222
223 #[test]
224 fn classify_ptrace_perf_is_benign() {
225 assert!(!classify_ptrace("perf", "app"));
226 }
227
228 #[test]
229 fn classify_ptrace_unknown_tracing_login_suspicious() {
230 assert!(classify_ptrace("injector", "login"));
231 }
232
233 #[test]
234 fn classify_ptrace_unknown_tracing_sudo_suspicious() {
235 assert!(classify_ptrace("spyware", "sudo"));
236 }
237
238 #[test]
239 fn classify_ptrace_unknown_tracing_su_suspicious() {
240 assert!(classify_ptrace("spyware", "su"));
241 }
242
243 #[test]
244 fn classify_ptrace_unknown_tracing_gpg_agent_suspicious() {
245 assert!(classify_ptrace("spyware", "gpg-agent"));
246 }
247
248 #[test]
249 fn scan_ptrace_nonzero_flags_parent_equals_real_parent_skipped() {
250 use memf_core::test_builders::flags as ptf;
251
252 let task_vaddr: u64 = 0xFFFF_8000_0020_0000;
253 let task_paddr: u64 = 0x0090_0000;
254 let parent_vaddr: u64 = 0xFFFF_8000_0030_0000;
255
256 let mut data = vec![0u8; 512];
257 data[8..12].copy_from_slice(&1u32.to_le_bytes());
258 data[16..24].copy_from_slice(&parent_vaddr.to_le_bytes());
259 data[24..32].copy_from_slice(&parent_vaddr.to_le_bytes());
260
261 let isf = IsfBuilder::new()
262 .add_struct("task_struct", 256)
263 .add_field("task_struct", "pid", 0, "int")
264 .add_field("task_struct", "ptrace", 8, "unsigned int")
265 .add_field("task_struct", "parent", 16, "pointer")
266 .add_field("task_struct", "real_parent", 24, "pointer")
267 .add_field("task_struct", "comm", 32, "char");
268 let ptb = PageTableBuilder::new()
269 .map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
270 .write_phys(task_paddr, &data);
271 let reader = make_reader(&isf, ptb);
272
273 let proc = fake_process(300, "victim", task_vaddr);
274 let result = scan_ptrace_relationships(&reader, &[proc]).unwrap();
275 assert!(result.is_empty());
276 }
277
278 #[test]
279 fn scan_ptrace_nonzero_flags_parent_is_null_skipped() {
280 use memf_core::test_builders::flags as ptf;
281
282 let task_vaddr: u64 = 0xFFFF_8000_0040_0000;
283 let task_paddr: u64 = 0x00A0_0000;
284
285 let mut data = vec![0u8; 512];
286 data[8..12].copy_from_slice(&1u32.to_le_bytes());
287 data[16..24].copy_from_slice(&0u64.to_le_bytes());
288 data[24..32].copy_from_slice(&0xFFFF_8000_0050_0000u64.to_le_bytes());
289
290 let isf = IsfBuilder::new()
291 .add_struct("task_struct", 256)
292 .add_field("task_struct", "pid", 0, "int")
293 .add_field("task_struct", "ptrace", 8, "unsigned int")
294 .add_field("task_struct", "parent", 16, "pointer")
295 .add_field("task_struct", "real_parent", 24, "pointer")
296 .add_field("task_struct", "comm", 32, "char");
297 let ptb = PageTableBuilder::new()
298 .map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
299 .write_phys(task_paddr, &data);
300 let reader = make_reader(&isf, ptb);
301
302 let proc = fake_process(400, "victim", task_vaddr);
303 let result = scan_ptrace_relationships(&reader, &[proc]).unwrap();
304 assert!(result.is_empty());
305 }
306
307 #[test]
308 fn scan_ptrace_detects_reparented_tracer() {
309 use memf_core::test_builders::flags as ptf;
310
311 let tracee_vaddr: u64 = 0xFFFF_8000_0060_0000;
312 let tracee_paddr: u64 = 0x00C0_0000;
313 let tracer_vaddr: u64 = 0xFFFF_8000_0061_0000;
314 let tracer_paddr: u64 = 0x00C1_0000;
315 let real_parent_vaddr: u64 = 0xFFFF_8000_0062_0000;
316
317 let mut tracee_data = vec![0u8; 512];
318 tracee_data[0..4].copy_from_slice(&555u64.to_le_bytes()[..4]);
319 tracee_data[8..12].copy_from_slice(&1u32.to_le_bytes());
320 tracee_data[16..24].copy_from_slice(&tracer_vaddr.to_le_bytes());
321 tracee_data[24..32].copy_from_slice(&real_parent_vaddr.to_le_bytes());
322 tracee_data[32..36].copy_from_slice(b"sshd");
323
324 let mut tracer_data = vec![0u8; 512];
325 tracer_data[0..8].copy_from_slice(&777u64.to_le_bytes());
326 tracer_data[32..40].copy_from_slice(b"injector");
327
328 let isf = IsfBuilder::new()
329 .add_struct("task_struct", 256)
330 .add_field("task_struct", "pid", 0, "long")
331 .add_field("task_struct", "ptrace", 8, "unsigned int")
332 .add_field("task_struct", "parent", 16, "pointer")
333 .add_field("task_struct", "real_parent", 24, "pointer")
334 .add_field("task_struct", "comm", 32, "char");
335
336 let ptb = PageTableBuilder::new()
337 .map_4k(tracee_vaddr, tracee_paddr, ptf::WRITABLE)
338 .write_phys(tracee_paddr, &tracee_data)
339 .map_4k(tracer_vaddr, tracer_paddr, ptf::WRITABLE)
340 .write_phys(tracer_paddr, &tracer_data);
341
342 let reader = make_reader(&isf, ptb);
343
344 let proc = fake_process(555, "sshd", tracee_vaddr);
345 let result = scan_ptrace_relationships(&reader, &[proc]).unwrap();
346
347 assert_eq!(result.len(), 1, "reparenting detected → one relationship");
348 let rel = &result[0];
349 assert_eq!(rel.tracer_pid, 777);
350 assert_eq!(rel.tracer_name, "injector");
351 assert_eq!(rel.tracee_pid, 555);
352 assert_eq!(rel.tracee_name, "sshd");
353 assert!(rel.is_suspicious);
354 }
355
356 #[test]
357 fn ptrace_relationship_serializes() {
358 let rel = PtraceRelationship {
359 tracer_pid: 42,
360 tracer_name: "evil".to_string(),
361 tracee_pid: 100,
362 tracee_name: "sshd".to_string(),
363 is_suspicious: true,
364 };
365 let json = serde_json::to_string(&rel).unwrap();
366 assert!(json.contains("\"tracer_pid\":42"));
367 assert!(json.contains("\"is_suspicious\":true"));
368 }
369}