Skip to main content

memf_linux/
psxview.rs

1//! Linux hidden process detection via cross-view analysis.
2//!
3//! Compares process visibility across multiple kernel data structures:
4//! the `task_struct` linked list and the PID hash table (`pid_hash` or
5//! `pidhash`). Processes missing from one view but present in another
6//! may have been hidden via Direct Kernel Object Manipulation (DKOM).
7
8use memf_core::object_reader::ObjectReader;
9use memf_format::PhysicalMemoryProvider;
10
11use crate::{Error, PsxViewInfo, Result};
12
13/// Cross-reference process visibility across kernel data structures.
14pub fn walk_psxview<P: PhysicalMemoryProvider>(
15    reader: &ObjectReader<P>,
16) -> Result<Vec<PsxViewInfo>> {
17    let init_task_addr = reader
18        .symbols()
19        .symbol_address("init_task")
20        .ok_or_else(|| Error::MissingKernelSymbol {
21            name: "init_task".into(),
22        })?;
23
24    let tasks_offset = reader
25        .symbols()
26        .field_offset("task_struct", "tasks")
27        .ok_or_else(|| Error::MissingField {
28            struct_name: "task_struct".into(),
29            field_name: "tasks".into(),
30        })?;
31
32    let head_vaddr = init_task_addr + tasks_offset;
33    let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
34
35    // Build set of PIDs found in the PID hash table.
36    // If the hash table is unavailable (no symbol, unreadable memory, missing ISF
37    // fields, or completely empty — which is impossible on a live system), we fall
38    // back to assuming all PIDs are present (in_pid_hash = true).
39    let pid_hash_pids = collect_pid_hash_pids(reader).filter(|s| !s.is_empty());
40
41    let mut results = Vec::new();
42
43    if let Ok(info) = read_task_info(reader, init_task_addr) {
44        let in_pid_hash = pid_hash_pids
45            .as_ref()
46            .map_or(true, |set| set.contains(&info.0));
47        results.push(PsxViewInfo {
48            pid: info.0,
49            comm: info.1,
50            in_task_list: true,
51            in_pid_hash,
52        });
53    }
54
55    for &task_addr in &task_addrs {
56        if let Ok(info) = read_task_info(reader, task_addr) {
57            let in_pid_hash = pid_hash_pids
58                .as_ref()
59                .map_or(true, |set| set.contains(&info.0));
60            results.push(PsxViewInfo {
61                pid: info.0,
62                comm: info.1,
63                in_task_list: true,
64                in_pid_hash,
65            });
66        }
67    }
68
69    Ok(results)
70}
71
72/// Walk the PID hash table and collect every PID found there.
73///
74/// Linux maintains a hash table (`pid_hash`) indexed by PID value. Each bucket
75/// is an `hlist_head` — a pointer to the first `hlist_node`. Each `task_struct`
76/// embeds an `hlist_node` in its `pid_links` field. By walking every non-empty
77/// bucket and following `hlist_node.next` chains we get the set of PIDs visible
78/// in the hash. A process absent from this set but present in the task list has
79/// been hidden via DKOM (Direct Kernel Object Manipulation).
80///
81/// Returns `None` when the symbol or required ISF fields are unavailable (the
82/// caller should fall back to `in_pid_hash = true`).
83fn collect_pid_hash_pids<P: PhysicalMemoryProvider>(
84    reader: &ObjectReader<P>,
85) -> Option<std::collections::HashSet<u64>> {
86    // Require the pid_hash symbol and hlist_node/pid_links field offsets.
87    let pid_hash_addr = reader.symbols().symbol_address("pid_hash")?;
88    let pid_links_offset = reader.symbols().field_offset("task_struct", "pid_links")?;
89    let hlist_next_offset = reader.symbols().field_offset("hlist_node", "next")?;
90
91    let mut found_pids = std::collections::HashSet::new();
92
93    // pid_hash is an array of hlist_head structs (each is a single pointer).
94    // We scan until we hit an unreadable address; bucket count is not in ISF
95    // so we probe until the first read failure.
96    let mut bucket_offset: u64 = 0;
97    loop {
98        let bucket_head_addr = pid_hash_addr.wrapping_add(bucket_offset);
99        // Each hlist_head is a single pointer to the first hlist_node (or NULL).
100        let first_node_ptr: u64 = match reader
101            .read_bytes(bucket_head_addr, 8)
102            .ok()
103            .and_then(|b| b.try_into().ok())
104            .map(u64::from_le_bytes)
105        {
106            Some(v) => v,
107            None => break,
108        };
109
110        // Walk the hlist_node chain for this bucket.
111        let mut node_ptr = first_node_ptr;
112        let mut depth = 0usize;
113        while node_ptr != 0 && depth < 10_000 {
114            // task_struct base = hlist_node address − pid_links_offset
115            let task_addr = node_ptr.wrapping_sub(pid_links_offset);
116            if let Ok(pid) = reader.read_field::<u32>(task_addr, "task_struct", "pid") {
117                found_pids.insert(u64::from(pid));
118            }
119            // Advance to hlist_node.next
120            let next_ptr: u64 = match reader
121                .read_bytes(node_ptr.wrapping_add(hlist_next_offset), 8)
122                .ok()
123                .and_then(|b| b.try_into().ok())
124                .map(u64::from_le_bytes)
125            {
126                Some(v) => v,
127                None => break,
128            };
129            node_ptr = next_ptr;
130            depth += 1;
131        }
132
133        bucket_offset = bucket_offset.wrapping_add(8);
134        // Stop after scanning a reasonable upper bound of buckets (typically
135        // pid_hash has 4096 buckets on a 64-bit system). We stop early on
136        // read failure so this cap just prevents runaway on synthetic memory.
137        if bucket_offset >= 8 * 4096 {
138            break;
139        }
140    }
141
142    Some(found_pids)
143}
144
145fn read_task_info<P: PhysicalMemoryProvider>(
146    reader: &ObjectReader<P>,
147    task_addr: u64,
148) -> Result<(u64, String)> {
149    let pid: u32 = reader.read_field(task_addr, "task_struct", "pid")?;
150    let comm = reader.read_field_string(task_addr, "task_struct", "comm", 16)?;
151    Ok((u64::from(pid), comm))
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
158    use memf_core::vas::{TranslationMode, VirtualAddressSpace};
159    use memf_symbols::isf::IsfResolver;
160    use memf_symbols::test_builders::IsfBuilder;
161
162    fn make_test_reader(
163        data: &[u8],
164        vaddr: u64,
165        paddr: u64,
166        extra_mappings: &[(u64, u64, &[u8])],
167    ) -> ObjectReader<SyntheticPhysMem> {
168        let isf = IsfBuilder::new()
169            .add_struct("task_struct", 128)
170            .add_field("task_struct", "pid", 0, "int")
171            .add_field("task_struct", "state", 4, "long")
172            .add_field("task_struct", "tasks", 16, "list_head")
173            .add_field("task_struct", "comm", 32, "char")
174            .add_field("task_struct", "mm", 48, "pointer")
175            .add_field("task_struct", "pid_links", 56, "hlist_node")
176            .add_struct("list_head", 16)
177            .add_field("list_head", "next", 0, "pointer")
178            .add_field("list_head", "prev", 8, "pointer")
179            .add_struct("hlist_node", 16)
180            .add_field("hlist_node", "next", 0, "pointer")
181            .add_field("hlist_node", "pprev", 8, "pointer")
182            .add_struct("pid", 32)
183            .add_field("pid", "nr", 0, "unsigned int")
184            .add_symbol("init_task", vaddr)
185            .add_symbol("pid_hash", vaddr + 0x800)
186            .build_json();
187
188        let resolver = IsfResolver::from_value(&isf).unwrap();
189        let mut builder = PageTableBuilder::new()
190            .map_4k(vaddr, paddr, ptflags::WRITABLE)
191            .write_phys(paddr, data);
192
193        for &(ev, ep, edata) in extra_mappings {
194            builder = builder
195                .map_4k(ev, ep, ptflags::WRITABLE)
196                .write_phys(ep, edata);
197        }
198
199        let (cr3, mem) = builder.build();
200        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
201        ObjectReader::new(vas, Box::new(resolver))
202    }
203
204    #[test]
205    fn all_processes_visible_in_both_views() {
206        let vaddr: u64 = 0xFFFF_8000_0010_0000;
207        let paddr: u64 = 0x0080_0000;
208        let mut data = vec![0u8; 4096];
209
210        data[0..4].copy_from_slice(&1u32.to_le_bytes());
211        let tasks_addr = vaddr + 16;
212        data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
213        data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
214        data[32..36].copy_from_slice(b"init");
215
216        let reader = make_test_reader(&data, vaddr, paddr, &[]);
217        let results = walk_psxview(&reader).unwrap();
218
219        assert!(!results.is_empty());
220        assert!(results[0].in_task_list);
221    }
222
223    #[test]
224    fn missing_init_task_symbol() {
225        let isf = IsfBuilder::new()
226            .add_struct("task_struct", 64)
227            .add_field("task_struct", "pid", 0, "int")
228            .add_field("task_struct", "tasks", 8, "list_head")
229            .add_struct("list_head", 16)
230            .add_field("list_head", "next", 0, "pointer")
231            .add_field("list_head", "prev", 8, "pointer")
232            .build_json();
233
234        let resolver = IsfResolver::from_value(&isf).unwrap();
235        let (cr3, mem) = PageTableBuilder::new().build();
236        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
237        let reader = ObjectReader::new(vas, Box::new(resolver));
238
239        let result = walk_psxview(&reader);
240        assert!(
241            matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "init_task"),
242            "expected MissingKernelSymbol {{name: \"init_task\"}}, got {result:?}"
243        );
244    }
245
246    #[test]
247    fn missing_tasks_field_returns_missing_field() {
248        let isf = IsfBuilder::new()
249            .add_struct("task_struct", 128)
250            .add_field("task_struct", "pid", 0, "int")
251            .add_field("task_struct", "comm", 32, "char")
252            .add_struct("list_head", 16)
253            .add_field("list_head", "next", 0, "pointer")
254            .add_field("list_head", "prev", 8, "pointer")
255            .add_symbol("init_task", 0xFFFF_8000_0010_0000)
256            .build_json();
257
258        let resolver = IsfResolver::from_value(&isf).unwrap();
259        let (cr3, mem) = PageTableBuilder::new().build();
260        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
261        let reader = ObjectReader::new(vas, Box::new(resolver));
262
263        let result = walk_psxview(&reader);
264        assert!(
265            matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "task_struct" && field_name == "tasks"),
266            "expected MissingField task_struct.tasks, got {result:?}"
267        );
268    }
269
270    #[test]
271    fn walk_psxview_multiple_tasks_in_list() {
272        let init_vaddr: u64 = 0xFFFF_8000_0020_0000;
273        let init_paddr: u64 = 0x0090_0000;
274        let task2_vaddr: u64 = 0xFFFF_8000_0021_0000;
275        let task2_paddr: u64 = 0x0091_0000;
276
277        let mut init_data = vec![0u8; 4096];
278        init_data[0..4].copy_from_slice(&1u32.to_le_bytes());
279        let task2_tasks = task2_vaddr + 16;
280        init_data[16..24].copy_from_slice(&task2_tasks.to_le_bytes());
281        init_data[24..32].copy_from_slice(&task2_tasks.to_le_bytes());
282        init_data[32..38].copy_from_slice(b"init\0\0");
283
284        let mut task2_data = vec![0u8; 4096];
285        task2_data[0..4].copy_from_slice(&2u32.to_le_bytes());
286        let init_tasks = init_vaddr + 16;
287        task2_data[16..24].copy_from_slice(&init_tasks.to_le_bytes());
288        task2_data[24..32].copy_from_slice(&init_tasks.to_le_bytes());
289        task2_data[32..36].copy_from_slice(b"sh\0\0");
290
291        let isf = IsfBuilder::new()
292            .add_struct("task_struct", 128)
293            .add_field("task_struct", "pid", 0, "int")
294            .add_field("task_struct", "state", 4, "long")
295            .add_field("task_struct", "tasks", 16, "list_head")
296            .add_field("task_struct", "comm", 32, "char")
297            .add_struct("list_head", 16)
298            .add_field("list_head", "next", 0, "pointer")
299            .add_field("list_head", "prev", 8, "pointer")
300            .add_symbol("init_task", init_vaddr)
301            .build_json();
302
303        let resolver = IsfResolver::from_value(&isf).unwrap();
304        let (cr3, mem) = PageTableBuilder::new()
305            .map_4k(init_vaddr, init_paddr, ptflags::WRITABLE)
306            .write_phys(init_paddr, &init_data)
307            .map_4k(task2_vaddr, task2_paddr, ptflags::WRITABLE)
308            .write_phys(task2_paddr, &task2_data)
309            .build();
310        let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
311        let reader = ObjectReader::new(vas, Box::new(resolver));
312
313        let results = walk_psxview(&reader).unwrap();
314
315        assert_eq!(results.len(), 2, "expected two tasks: init + task2");
316
317        let init_entry = results
318            .iter()
319            .find(|r| r.pid == 1)
320            .expect("init_task missing");
321        assert!(init_entry.in_task_list);
322        assert!(init_entry.in_pid_hash);
323
324        let task2_entry = results.iter().find(|r| r.pid == 2).expect("task2 missing");
325        assert!(task2_entry.in_task_list);
326        assert!(task2_entry.in_pid_hash);
327        assert_eq!(task2_entry.comm, "sh");
328    }
329
330    #[test]
331    fn psxview_entries_have_correct_visibility_flags() {
332        let vaddr: u64 = 0xFFFF_8000_0010_0000;
333        let paddr: u64 = 0x0080_0000;
334        let mut data = vec![0u8; 4096];
335
336        data[0..4].copy_from_slice(&1u32.to_le_bytes());
337        let tasks_addr = vaddr + 16;
338        data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
339        data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
340        data[32..39].copy_from_slice(b"swapper");
341
342        let reader = make_test_reader(&data, vaddr, paddr, &[]);
343        let results = walk_psxview(&reader).unwrap();
344
345        assert!(!results.is_empty(), "should find at least init_task");
346        let init = &results[0];
347        assert!(init.in_task_list, "init_task must be in_task_list");
348        assert!(init.in_pid_hash, "init_task must be in_pid_hash");
349        assert_eq!(init.pid, 1);
350    }
351}