1use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::{vma_walker::for_each_task_vma, Error, MalfindInfo, Result};
11
12const HEADER_SIZE: usize = 64;
14
15pub fn scan_malfind<P: PhysicalMemoryProvider>(
21 reader: &ObjectReader<P>,
22) -> Result<Vec<MalfindInfo>> {
23 let init_task_addr = reader
24 .symbols()
25 .symbol_address("init_task")
26 .ok_or_else(|| Error::MissingKernelSymbol {
27 name: "init_task".into(),
28 })?;
29
30 let tasks_offset = reader
31 .symbols()
32 .field_offset("task_struct", "tasks")
33 .ok_or_else(|| Error::MissingField {
34 struct_name: "task_struct".into(),
35 field_name: "tasks".into(),
36 })?;
37
38 let head_vaddr = init_task_addr + tasks_offset;
39 let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
40
41 let mut findings = Vec::new();
42
43 scan_process_vmas(reader, init_task_addr, &mut findings);
45
46 for &task_addr in &task_addrs {
47 scan_process_vmas(reader, task_addr, &mut findings);
48 }
49
50 Ok(findings)
51}
52
53fn scan_process_vmas<P: PhysicalMemoryProvider>(
55 reader: &ObjectReader<P>,
56 task_addr: u64,
57 out: &mut Vec<MalfindInfo>,
58) {
59 let pid: u32 = match reader.read_field(task_addr, "task_struct", "pid") {
60 Ok(v) => v,
61 Err(_) => return,
62 };
63 let comm = reader
64 .read_field_string(task_addr, "task_struct", "comm", 16)
65 .unwrap_or_default();
66
67 for_each_task_vma(reader, task_addr, &mut |e| {
68 if let Some(f) = check_vma(reader, &e, u64::from(pid), &comm) {
69 out.push(f);
70 }
71 });
72}
73
74fn check_vma<P: PhysicalMemoryProvider>(
77 reader: &ObjectReader<P>,
78 entry: &crate::vma_walker::VmaEntry,
79 pid: u64,
80 comm: &str,
81) -> Option<MalfindInfo> {
82 let file_backed = entry.file_ptr != 0;
83
84 if !(entry.flags.write && entry.flags.exec && !file_backed) {
86 return None;
87 }
88
89 let read_size = HEADER_SIZE.min((entry.end - entry.start) as usize);
91 let header_bytes = reader
92 .read_bytes(entry.start, read_size)
93 .unwrap_or_default();
94
95 let reason = format!(
96 "anonymous rwx region ({} flags, {} bytes)",
97 entry.flags,
98 entry.end - entry.start
99 );
100
101 Some(MalfindInfo {
102 pid,
103 comm: comm.to_string(),
104 start: entry.start,
105 end: entry.end,
106 flags: entry.flags,
107 reason,
108 header_bytes,
109 })
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
116 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
117 use memf_symbols::isf::IsfResolver;
118 use memf_symbols::test_builders::IsfBuilder;
119
120 fn make_test_reader(
121 data: &[u8],
122 vaddr: u64,
123 paddr: u64,
124 extra_mappings: &[(u64, u64, &[u8])],
125 ) -> ObjectReader<SyntheticPhysMem> {
126 let isf = IsfBuilder::new()
127 .add_struct("task_struct", 128)
128 .add_field("task_struct", "pid", 0, "int")
129 .add_field("task_struct", "state", 4, "long")
130 .add_field("task_struct", "tasks", 16, "list_head")
131 .add_field("task_struct", "comm", 32, "char")
132 .add_field("task_struct", "mm", 48, "pointer")
133 .add_struct("list_head", 16)
134 .add_field("list_head", "next", 0, "pointer")
135 .add_field("list_head", "prev", 8, "pointer")
136 .add_struct("mm_struct", 128)
137 .add_field("mm_struct", "pgd", 0, "pointer")
138 .add_field("mm_struct", "mmap", 8, "pointer")
139 .add_struct("vm_area_struct", 64)
140 .add_field("vm_area_struct", "vm_start", 0, "unsigned long")
141 .add_field("vm_area_struct", "vm_end", 8, "unsigned long")
142 .add_field("vm_area_struct", "vm_next", 16, "pointer")
143 .add_field("vm_area_struct", "vm_flags", 24, "unsigned long")
144 .add_field("vm_area_struct", "vm_pgoff", 32, "unsigned long")
145 .add_field("vm_area_struct", "vm_file", 40, "pointer")
146 .add_symbol("init_task", vaddr)
147 .build_json();
148
149 let resolver = IsfResolver::from_value(&isf).unwrap();
150 let mut builder = PageTableBuilder::new()
151 .map_4k(vaddr, paddr, ptflags::WRITABLE)
152 .write_phys(paddr, data);
153
154 for &(ev, ep, edata) in extra_mappings {
155 builder = builder
156 .map_4k(ev, ep, ptflags::WRITABLE)
157 .write_phys(ep, edata);
158 }
159
160 let (cr3, mem) = builder.build();
161 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
162 ObjectReader::new(vas, Box::new(resolver))
163 }
164
165 #[test]
167 fn detects_rwx_anonymous_region() {
168 let vaddr: u64 = 0xFFFF_8000_0010_0000;
169 let paddr: u64 = 0x0080_0000;
170 let mut data = vec![0u8; 4096];
171
172 data[0..4].copy_from_slice(&1u32.to_le_bytes());
174 let tasks_addr = vaddr + 16;
175 data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
176 data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
177 data[32..38].copy_from_slice(b"victim");
178 let mm_addr = vaddr + 0x200;
179 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
180
181 data[0x200..0x208].copy_from_slice(&0x1000u64.to_le_bytes());
183 let vma1_addr = vaddr + 0x300;
184 data[0x208..0x210].copy_from_slice(&vma1_addr.to_le_bytes());
185
186 let code_start: u64 = 0xFFFF_8000_0020_0000;
188 data[0x300..0x308].copy_from_slice(&code_start.to_le_bytes());
189 data[0x308..0x310].copy_from_slice(&(code_start + 0x1000).to_le_bytes());
190 let vma2_addr = vaddr + 0x400;
191 data[0x310..0x318].copy_from_slice(&vma2_addr.to_le_bytes());
192 data[0x318..0x320].copy_from_slice(&0x5u64.to_le_bytes()); data[0x328..0x330].copy_from_slice(&0x9999u64.to_le_bytes()); let suspect_vaddr: u64 = 0xFFFF_8000_0030_0000;
197 let suspect_paddr: u64 = 0x0090_0000;
198 data[0x400..0x408].copy_from_slice(&suspect_vaddr.to_le_bytes());
199 data[0x408..0x410].copy_from_slice(&(suspect_vaddr + 0x1000).to_le_bytes());
200 data[0x410..0x418].copy_from_slice(&0u64.to_le_bytes()); data[0x418..0x420].copy_from_slice(&0x7u64.to_le_bytes()); data[0x420..0x428].copy_from_slice(&0u64.to_le_bytes()); data[0x428..0x430].copy_from_slice(&0u64.to_le_bytes()); let mut suspect_data = vec![0u8; 4096];
207 suspect_data[0] = b'M';
208 suspect_data[1] = b'Z';
209 suspect_data[2..64].fill(0x90); let reader = make_test_reader(
212 &data,
213 vaddr,
214 paddr,
215 &[(suspect_vaddr, suspect_paddr, &suspect_data)],
216 );
217 let findings = scan_malfind(&reader).unwrap();
218
219 assert_eq!(findings.len(), 1);
220 assert_eq!(findings[0].pid, 1);
221 assert_eq!(findings[0].comm, "victim");
222 assert_eq!(findings[0].start, suspect_vaddr);
223 assert!(findings[0].flags.write);
224 assert!(findings[0].flags.exec);
225 assert_eq!(findings[0].header_bytes[0], b'M');
226 assert_eq!(findings[0].header_bytes[1], b'Z');
227 assert!(findings[0].reason.contains("anonymous"));
228 }
229
230 #[test]
232 fn ignores_file_backed_rwx() {
233 let vaddr: u64 = 0xFFFF_8000_0010_0000;
234 let paddr: u64 = 0x0080_0000;
235 let mut data = vec![0u8; 4096];
236
237 data[0..4].copy_from_slice(&1u32.to_le_bytes());
238 let tasks_addr = vaddr + 16;
239 data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
240 data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
241 data[32..36].copy_from_slice(b"test");
242 let mm_addr = vaddr + 0x200;
243 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
244
245 data[0x200..0x208].copy_from_slice(&0x1000u64.to_le_bytes());
246 let vma_addr = vaddr + 0x300;
247 data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes());
248
249 data[0x300..0x308].copy_from_slice(&0x0040_0000u64.to_le_bytes());
251 data[0x308..0x310].copy_from_slice(&0x0040_1000u64.to_le_bytes());
252 data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes());
253 data[0x318..0x320].copy_from_slice(&0x7u64.to_le_bytes()); data[0x328..0x330].copy_from_slice(&0xABCDu64.to_le_bytes()); let reader = make_test_reader(&data, vaddr, paddr, &[]);
257 let findings = scan_malfind(&reader).unwrap();
258
259 assert!(findings.is_empty());
260 }
261
262 #[test]
263 fn skips_kernel_threads() {
264 let vaddr: u64 = 0xFFFF_8000_0010_0000;
265 let paddr: u64 = 0x0080_0000;
266 let mut data = vec![0u8; 4096];
267
268 data[0..4].copy_from_slice(&0u32.to_le_bytes());
269 let tasks_addr = vaddr + 16;
270 data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
271 data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
272 data[32..41].copy_from_slice(b"swapper/0");
273 data[48..56].copy_from_slice(&0u64.to_le_bytes()); let reader = make_test_reader(&data, vaddr, paddr, &[]);
276 let findings = scan_malfind(&reader).unwrap();
277
278 assert!(findings.is_empty());
279 }
280
281 #[test]
282 fn missing_init_task_symbol() {
283 let isf = IsfBuilder::new()
284 .add_struct("task_struct", 64)
285 .add_field("task_struct", "pid", 0, "int")
286 .add_field("task_struct", "tasks", 8, "list_head")
287 .add_struct("list_head", 16)
288 .add_field("list_head", "next", 0, "pointer")
289 .add_field("list_head", "prev", 8, "pointer")
290 .build_json();
291
292 let resolver = IsfResolver::from_value(&isf).unwrap();
293 let (cr3, mem) = PageTableBuilder::new().build();
294 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
295 let reader = ObjectReader::new(vas, Box::new(resolver));
296
297 let result = scan_malfind(&reader);
298 assert!(
299 matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "init_task"),
300 "expected MissingKernelSymbol {{name: \"init_task\"}}, got {result:?}"
301 );
302 }
303
304 #[test]
305 fn missing_tasks_field_returns_missing_field() {
306 let isf = IsfBuilder::new()
307 .add_struct("task_struct", 64)
308 .add_field("task_struct", "pid", 0, "int")
309 .add_struct("list_head", 16)
311 .add_field("list_head", "next", 0, "pointer")
312 .add_field("list_head", "prev", 8, "pointer")
313 .add_symbol("init_task", 0xFFFF_8000_0010_0000)
314 .build_json();
315 let resolver = IsfResolver::from_value(&isf).unwrap();
316 let (cr3, mem) = PageTableBuilder::new().build();
317 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
318 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
319 let result = scan_malfind(&reader);
320 assert!(
321 matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "task_struct" && field_name == "tasks"),
322 "expected MissingField task_struct.tasks, got {result:?}"
323 );
324 }
325}