1use memf_core::object_reader::ObjectReader;
4use memf_format::PhysicalMemoryProvider;
5
6use crate::types::HiddenProcessInfo;
7use crate::Result;
8
9pub fn is_dkom_hidden(in_task_list: bool, in_pid_hash: bool) -> bool {
16 !in_task_list || !in_pid_hash
17}
18
19pub fn find_hidden_processes<P: PhysicalMemoryProvider>(
24 reader: &ObjectReader<P>,
25) -> Result<Vec<HiddenProcessInfo>> {
26 let entries = match crate::psxview::walk_psxview(reader) {
27 Ok(v) => v,
28 Err(crate::Error::MissingKernelSymbol { .. } | crate::Error::MissingField { .. }) => {
29 return Ok(vec![]);
30 }
31 Err(e) => return Err(e),
32 };
33 Ok(entries
34 .into_iter()
35 .filter(|e| is_dkom_hidden(e.in_task_list, e.in_pid_hash))
36 .map(|e| HiddenProcessInfo {
37 pid: e.pid,
38 comm: e.comm,
39 present_in_pid_ns: false,
40 present_in_task_list: e.in_task_list,
41 present_in_pid_hash: e.in_pid_hash,
42 })
43 .collect())
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49 use memf_core::test_builders::PageTableBuilder;
50 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
51 use memf_symbols::isf::IsfResolver;
52 use memf_symbols::test_builders::IsfBuilder;
53
54 fn make_minimal_reader() -> ObjectReader<memf_core::test_builders::SyntheticPhysMem> {
55 let isf = IsfBuilder::new().build_json();
56 let resolver = IsfResolver::from_value(&isf).unwrap();
57 let (cr3, mem) = PageTableBuilder::new().build();
58 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
59 ObjectReader::new(vas, Box::new(resolver))
60 }
61
62 #[test]
63 fn empty_memory_returns_ok_empty() {
64 let reader = make_minimal_reader();
65 let result = find_hidden_processes(&reader);
66 assert!(result.is_ok(), "should succeed with minimal reader");
67 assert!(
68 result.unwrap().is_empty(),
69 "empty memory → no hidden processes"
70 );
71 }
72
73 #[test]
74 fn result_is_vec_of_hidden_process_info() {
75 let reader = make_minimal_reader();
76 let result: Result<Vec<HiddenProcessInfo>> = find_hidden_processes(&reader);
77 assert!(result.is_ok());
78 }
79
80 #[test]
81 fn hidden_process_info_fields_constructible() {
82 let info = HiddenProcessInfo {
83 pid: 1234,
84 comm: "evil".to_string(),
85 present_in_pid_ns: false,
86 present_in_task_list: true,
87 present_in_pid_hash: true,
88 };
89 assert_eq!(info.pid, 1234);
90 assert_eq!(info.comm, "evil");
91 assert!(!info.present_in_pid_ns);
92 assert!(info.present_in_task_list);
93 assert!(info.present_in_pid_hash);
94 }
95
96 #[test]
97 fn hidden_process_info_serializes() {
98 let info = HiddenProcessInfo {
99 pid: 42,
100 comm: "rootkit".to_string(),
101 present_in_pid_ns: false,
102 present_in_task_list: false,
103 present_in_pid_hash: true,
104 };
105 let json = serde_json::to_string(&info).unwrap();
106 assert!(json.contains("\"pid\":42"));
107 assert!(json.contains("rootkit"));
108 assert!(json.contains("\"present_in_pid_hash\":true"));
109 }
110
111 #[test]
114 fn process_missing_from_task_list_is_hidden() {
115 assert!(is_dkom_hidden(false, true));
116 }
117
118 #[test]
119 fn process_missing_from_pid_hash_is_hidden() {
120 assert!(is_dkom_hidden(true, false));
121 }
122
123 #[test]
124 fn process_in_all_sources_is_not_hidden() {
125 assert!(!is_dkom_hidden(true, true));
126 }
127
128 use memf_core::test_builders::{flags as ptflags, SyntheticPhysMem};
144
145 fn make_proc_hidden_reader(
146 page_data: &[u8; 4096],
147 vaddr: u64,
148 paddr: u64,
149 ) -> ObjectReader<SyntheticPhysMem> {
150 let isf = IsfBuilder::new()
151 .add_struct("task_struct", 128)
152 .add_field("task_struct", "pid", 0, "int")
153 .add_field("task_struct", "state", 4, "long")
154 .add_field("task_struct", "tasks", 16, "list_head")
155 .add_field("task_struct", "comm", 32, "char")
156 .add_field("task_struct", "mm", 48, "pointer")
157 .add_field("task_struct", "pid_links", 56, "hlist_node")
158 .add_struct("list_head", 16)
159 .add_field("list_head", "next", 0, "pointer")
160 .add_field("list_head", "prev", 8, "pointer")
161 .add_struct("hlist_node", 16)
162 .add_field("hlist_node", "next", 0, "pointer")
163 .add_field("hlist_node", "pprev", 8, "pointer")
164 .add_symbol("init_task", vaddr)
165 .add_symbol("pid_hash", vaddr + 0x800)
166 .build_json();
167 let resolver = IsfResolver::from_value(&isf).unwrap();
168 let (cr3, mem) = PageTableBuilder::new()
169 .map_4k(vaddr, paddr, ptflags::WRITABLE)
170 .write_phys(paddr, page_data)
171 .build();
172 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
173 ObjectReader::new(vas, Box::new(resolver))
174 }
175
176 fn base_task_page(vaddr: u64) -> [u8; 4096] {
177 let mut page = [0u8; 4096];
178 page[0..4].copy_from_slice(&1u32.to_le_bytes()); let tasks_va = vaddr + 16;
180 page[16..24].copy_from_slice(&tasks_va.to_le_bytes()); page[24..32].copy_from_slice(&tasks_va.to_le_bytes()); page[32..36].copy_from_slice(b"init"); page
184 }
185
186 #[test]
188 fn find_hidden_processes_detects_task_list_only_process() {
189 let vaddr: u64 = 0xFFFF_8000_0010_0000;
190 let paddr: u64 = 0x0080_0000;
191 let mut page = base_task_page(vaddr);
201 page[0x200..0x204].copy_from_slice(&99u32.to_le_bytes()); let fake_pid_links_va = vaddr + 0x238;
203 page[0x800..0x808].copy_from_slice(&fake_pid_links_va.to_le_bytes()); page[0x238..0x240].copy_from_slice(&0u64.to_le_bytes());
206 page[0x240..0x248].copy_from_slice(&(vaddr + 0x800).to_le_bytes());
208 let reader = make_proc_hidden_reader(&page, vaddr, paddr);
209
210 let hidden = find_hidden_processes(&reader).unwrap();
211 assert_eq!(hidden.len(), 1, "init absent from pid_hash must be flagged");
212 assert_eq!(hidden[0].pid, 1);
213 assert!(hidden[0].present_in_task_list);
214 assert!(!hidden[0].present_in_pid_hash);
215 }
216
217 #[test]
219 fn find_hidden_processes_clean_image_all_visible_returns_empty() {
220 let vaddr: u64 = 0xFFFF_8000_0010_0000;
221 let paddr: u64 = 0x0080_0000;
222 let mut page = base_task_page(vaddr);
223 let pid_links_va = vaddr + 56;
225 page[0x800..0x808].copy_from_slice(&pid_links_va.to_le_bytes());
226 page[0x808..0x810].copy_from_slice(&(vaddr + 0x800).to_le_bytes());
228 let reader = make_proc_hidden_reader(&page, vaddr, paddr);
229
230 let hidden = find_hidden_processes(&reader).unwrap();
231 assert!(
232 hidden.is_empty(),
233 "process visible in all sources must not be flagged"
234 );
235 }
236}