Skip to main content

memf_linux/
ptrace.rs

1//! Linux ptrace relationship detection for debugging/injection analysis.
2//!
3//! `ptrace` is the Linux debugging/tracing syscall. Attackers use it for
4//! process injection (`PTRACE_POKETEXT`), anti-debugging (tracing themselves),
5//! and credential theft (intercepting syscalls of privileged processes).
6//!
7//! This module detects active ptrace relationships by inspecting
8//! `task_struct.ptrace` flags and comparing `parent` vs `real_parent`
9//! pointers (ptrace reparents the tracee under the tracer).
10
11use memf_core::object_reader::ObjectReader;
12use memf_format::PhysicalMemoryProvider;
13
14use crate::{ProcessInfo, Result};
15
16/// A detected ptrace relationship between a tracer and a tracee process.
17#[derive(Debug, Clone, serde::Serialize)]
18pub struct PtraceRelationship {
19    /// PID of the process performing the trace.
20    pub tracer_pid: u32,
21    /// Name of the tracer process.
22    pub tracer_name: String,
23    /// PID of the process being traced.
24    pub tracee_pid: u32,
25    /// Name of the tracee process.
26    pub tracee_name: String,
27    /// True if this ptrace relationship is anomalous (non-debugger tracing a high-value target).
28    pub is_suspicious: bool,
29}
30
31/// Classify whether a ptrace relationship is suspicious.
32pub use crate::heuristics::classify_ptrace;
33
34/// Scan for active ptrace relationships across the given process list.
35pub 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}