1use memf_core::object_reader::ObjectReader;
10use memf_format::PhysicalMemoryProvider;
11
12use crate::{Error, Result, SshKeyInfo, SshKeyType, VmaFlags};
13
14const SSH_KEY_PREFIXES: &[(&str, SshKeyType)] = &[
16 ("ssh-rsa ", SshKeyType::Rsa),
17 ("ssh-ed25519 ", SshKeyType::Ed25519),
18 ("ssh-dss ", SshKeyType::Dsa),
19 ("ecdsa-sha2-nistp256 ", SshKeyType::Ecdsa256),
20 ("ecdsa-sha2-nistp384 ", SshKeyType::Ecdsa384),
21 ("ecdsa-sha2-nistp521 ", SshKeyType::Ecdsa521),
22];
23
24const MAX_KEY_LINE: usize = 8192;
26
27const MAX_VMA_SCAN: u64 = 16 * 1024 * 1024;
29
30pub fn extract_ssh_keys<P: PhysicalMemoryProvider>(
39 reader: &ObjectReader<P>,
40) -> Result<Vec<SshKeyInfo>> {
41 let init_task_addr = reader
42 .symbols()
43 .symbol_address("init_task")
44 .ok_or_else(|| Error::MissingKernelSymbol {
45 name: "init_task".into(),
46 })?;
47
48 let tasks_offset = reader
49 .symbols()
50 .field_offset("task_struct", "tasks")
51 .ok_or_else(|| Error::MissingField {
52 struct_name: "task_struct".into(),
53 field_name: "tasks".into(),
54 })?;
55
56 let head_vaddr = init_task_addr + tasks_offset;
57 let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
58
59 let mut results = Vec::new();
60 let mut seen = std::collections::HashSet::new();
61
62 scan_sshd_keys(reader, init_task_addr, &mut results, &mut seen);
64
65 for &task_addr in &task_addrs {
66 scan_sshd_keys(reader, task_addr, &mut results, &mut seen);
67 }
68
69 Ok(results)
70}
71
72fn scan_sshd_keys<P: PhysicalMemoryProvider>(
74 reader: &ObjectReader<P>,
75 task_addr: u64,
76 results: &mut Vec<SshKeyInfo>,
77 seen: &mut std::collections::HashSet<(u64, String)>,
78) {
79 let pid: u32 = match reader.read_field(task_addr, "task_struct", "pid") {
80 Ok(v) => v,
81 Err(_) => return,
82 };
83 let Ok(comm) = reader.read_field_string(task_addr, "task_struct", "comm", 16) else {
84 return;
85 };
86
87 if comm != "sshd" {
88 return;
89 }
90
91 let mm_ptr: u64 = match reader.read_field(task_addr, "task_struct", "mm") {
92 Ok(v) => v,
93 Err(_) => return,
94 };
95 if mm_ptr == 0 {
96 return; }
98
99 let mmap_ptr: u64 = match reader.read_field(mm_ptr, "mm_struct", "mmap") {
100 Ok(v) => v,
101 Err(_) => return,
102 };
103
104 let mut vma_addr = mmap_ptr;
106 let mut vma_count = 0u32;
107 while vma_addr != 0 && vma_count < 4096 {
108 vma_count += 1;
109
110 let vm_start: u64 = match reader.read_field(vma_addr, "vm_area_struct", "vm_start") {
111 Ok(v) => v,
112 Err(_) => break,
113 };
114 let vm_end: u64 = match reader.read_field(vma_addr, "vm_area_struct", "vm_end") {
115 Ok(v) => v,
116 Err(_) => break,
117 };
118 let vm_flags: u64 = match reader.read_field(vma_addr, "vm_area_struct", "vm_flags") {
119 Ok(v) => v,
120 Err(_) => break,
121 };
122
123 let flags = VmaFlags::from_raw(vm_flags);
124 let size = vm_end.saturating_sub(vm_start);
125
126 if flags.read && size > 0 && size <= MAX_VMA_SCAN {
128 scan_region_for_keys(reader, u64::from(pid), vm_start, size, results, seen);
129 }
130
131 vma_addr = match reader.read_field(vma_addr, "vm_area_struct", "vm_next") {
133 Ok(v) => v,
134 Err(_) => break,
135 };
136 }
137}
138
139fn scan_region_for_keys<P: PhysicalMemoryProvider>(
141 reader: &ObjectReader<P>,
142 pid: u64,
143 start: u64,
144 size: u64,
145 results: &mut Vec<SshKeyInfo>,
146 seen: &mut std::collections::HashSet<(u64, String)>,
147) {
148 let Ok(buf) = reader.read_bytes(start, size as usize) else {
149 return;
150 };
151
152 for &(prefix, _key_type) in SSH_KEY_PREFIXES {
153 let prefix_bytes = prefix.as_bytes();
154 let mut search_from = 0;
156 while search_from + prefix_bytes.len() <= buf.len() {
157 let haystack = &buf[search_from..];
158 let Some(pos) = find_bytes(haystack, prefix_bytes) else {
159 break;
160 };
161
162 let abs_pos = search_from + pos;
163
164 let line_start = abs_pos;
166 let max_end = buf.len().min(line_start + MAX_KEY_LINE);
167 let line_end = buf[line_start..max_end]
168 .iter()
169 .position(|&b| b == b'\n' || b == b'\0' || b == b'\r')
170 .map_or(max_end, |p| line_start + p);
171
172 let line_bytes = &buf[line_start..line_end];
173 if let Ok(line_str) = std::str::from_utf8(line_bytes) {
174 if let Some((key_type, key_data, comment)) = parse_key_line(line_str) {
175 let dedup_key = (pid, key_data.clone());
176 if seen.insert(dedup_key) {
177 results.push(SshKeyInfo {
178 pid,
179 key_type,
180 key_data,
181 comment,
182 });
183 }
184 }
185 }
186
187 search_from = abs_pos + prefix_bytes.len();
189 }
190 }
191}
192
193fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
195 haystack.windows(needle.len()).position(|w| w == needle)
196}
197
198fn parse_key_line(line: &str) -> Option<(SshKeyType, String, String)> {
202 let trimmed = line.trim();
203 if trimmed.is_empty() {
204 return None;
205 }
206
207 let space_idx = trimmed.find(' ')?;
209 let type_str = &trimmed[..space_idx];
210 let key_type = SshKeyType::from_prefix(type_str);
211 if key_type == SshKeyType::Unknown {
212 return None;
213 }
214
215 let rest = &trimmed[space_idx + 1..];
216
217 let (base64_data, comment) = match rest.find(' ') {
219 Some(idx) => (&rest[..idx], rest[idx + 1..].trim()),
220 None => (rest, ""),
221 };
222
223 if base64_data.is_empty() {
225 return None;
226 }
227
228 let full_key = format!("{type_str} {base64_data}");
229 Some((key_type, full_key, comment.to_string()))
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
236 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
237 use memf_symbols::isf::IsfResolver;
238 use memf_symbols::test_builders::IsfBuilder;
239
240 fn make_test_reader(
241 data: &[u8],
242 vaddr: u64,
243 paddr: u64,
244 extra_mappings: &[(u64, u64, &[u8])],
245 ) -> ObjectReader<SyntheticPhysMem> {
246 let isf = IsfBuilder::new()
247 .add_struct("task_struct", 128)
248 .add_field("task_struct", "pid", 0, "int")
249 .add_field("task_struct", "state", 4, "long")
250 .add_field("task_struct", "tasks", 16, "list_head")
251 .add_field("task_struct", "comm", 32, "char")
252 .add_field("task_struct", "mm", 48, "pointer")
253 .add_struct("list_head", 16)
254 .add_field("list_head", "next", 0, "pointer")
255 .add_field("list_head", "prev", 8, "pointer")
256 .add_struct("mm_struct", 128)
257 .add_field("mm_struct", "pgd", 0, "pointer")
258 .add_field("mm_struct", "mmap", 8, "pointer")
259 .add_struct("vm_area_struct", 64)
260 .add_field("vm_area_struct", "vm_start", 0, "unsigned long")
261 .add_field("vm_area_struct", "vm_end", 8, "unsigned long")
262 .add_field("vm_area_struct", "vm_next", 16, "pointer")
263 .add_field("vm_area_struct", "vm_flags", 24, "unsigned long")
264 .add_field("vm_area_struct", "vm_pgoff", 32, "unsigned long")
265 .add_field("vm_area_struct", "vm_file", 40, "pointer")
266 .add_symbol("init_task", vaddr)
267 .build_json();
268
269 let resolver = IsfResolver::from_value(&isf).unwrap();
270 let mut builder = PageTableBuilder::new()
271 .map_4k(vaddr, paddr, ptflags::WRITABLE)
272 .write_phys(paddr, data);
273
274 for &(ev, ep, edata) in extra_mappings {
275 builder = builder
276 .map_4k(ev, ep, ptflags::WRITABLE)
277 .write_phys(ep, edata);
278 }
279
280 let (cr3, mem) = builder.build();
281 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
282 ObjectReader::new(vas, Box::new(resolver))
283 }
284
285 #[test]
286 fn ssh_key_type_from_prefix() {
287 assert_eq!(SshKeyType::from_prefix("ssh-rsa"), SshKeyType::Rsa);
288 assert_eq!(SshKeyType::from_prefix("ssh-ed25519"), SshKeyType::Ed25519);
289 assert_eq!(SshKeyType::from_prefix("ssh-dss"), SshKeyType::Dsa);
290 assert_eq!(
291 SshKeyType::from_prefix("ecdsa-sha2-nistp256"),
292 SshKeyType::Ecdsa256
293 );
294 assert_eq!(
295 SshKeyType::from_prefix("ecdsa-sha2-nistp384"),
296 SshKeyType::Ecdsa384
297 );
298 assert_eq!(
299 SshKeyType::from_prefix("ecdsa-sha2-nistp521"),
300 SshKeyType::Ecdsa521
301 );
302 assert_eq!(SshKeyType::from_prefix("bogus"), SshKeyType::Unknown);
303 assert_eq!(SshKeyType::from_prefix(""), SshKeyType::Unknown);
304 }
305
306 #[test]
307 fn ssh_key_type_display() {
308 assert_eq!(SshKeyType::Rsa.to_string(), "ssh-rsa");
309 assert_eq!(SshKeyType::Ed25519.to_string(), "ssh-ed25519");
310 assert_eq!(SshKeyType::Dsa.to_string(), "ssh-dss");
311 assert_eq!(SshKeyType::Ecdsa256.to_string(), "ecdsa-sha2-nistp256");
312 assert_eq!(SshKeyType::Ecdsa384.to_string(), "ecdsa-sha2-nistp384");
313 assert_eq!(SshKeyType::Ecdsa521.to_string(), "ecdsa-sha2-nistp521");
314 assert_eq!(SshKeyType::Unknown.to_string(), "unknown");
315 }
316
317 #[test]
318 fn extract_ssh_keys_no_sshd() {
319 let vaddr: u64 = 0xFFFF_8000_0010_0000;
320 let paddr: u64 = 0x0080_0000;
321 let mut data = vec![0u8; 4096];
322
323 data[0..4].copy_from_slice(&1u32.to_le_bytes());
325 let tasks_addr = vaddr + 16;
326 data[16..24].copy_from_slice(&tasks_addr.to_le_bytes()); data[24..32].copy_from_slice(&tasks_addr.to_le_bytes()); data[32..39].copy_from_slice(b"systemd");
329 data[48..56].copy_from_slice(&0u64.to_le_bytes()); let reader = make_test_reader(&data, vaddr, paddr, &[]);
332 let results = extract_ssh_keys(&reader).unwrap();
333
334 assert!(results.is_empty());
335 }
336
337 #[test]
338 fn extracts_ed25519_key_from_sshd_heap() {
339 let vaddr: u64 = 0xFFFF_8000_0010_0000;
340 let paddr: u64 = 0x0080_0000;
341 let mut data = vec![0u8; 4096];
342
343 data[0..4].copy_from_slice(&22u32.to_le_bytes());
345 let tasks_addr = vaddr + 16;
346 data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
347 data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
348 data[32..36].copy_from_slice(b"sshd");
349 let mm_addr = vaddr + 0x200;
350 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
351
352 data[0x200..0x208].copy_from_slice(&0x1000u64.to_le_bytes()); let vma_addr = vaddr + 0x300;
355 data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes()); let heap_vaddr: u64 = 0x0000_5555_0000_0000;
359 let heap_paddr: u64 = 0x0090_0000;
360 data[0x300..0x308].copy_from_slice(&heap_vaddr.to_le_bytes()); data[0x308..0x310].copy_from_slice(&(heap_vaddr + 0x1000).to_le_bytes()); data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes()); data[0x318..0x320].copy_from_slice(&0x3u64.to_le_bytes()); data[0x320..0x328].copy_from_slice(&0u64.to_le_bytes()); data[0x328..0x330].copy_from_slice(&0u64.to_le_bytes()); let mut heap = vec![0u8; 4096];
369 let key_line = b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzBZ user@host\0";
370 heap[0x100..0x100 + key_line.len()].copy_from_slice(key_line);
371
372 let reader = make_test_reader(&data, vaddr, paddr, &[(heap_vaddr, heap_paddr, &heap)]);
373 let results = extract_ssh_keys(&reader).unwrap();
374
375 assert_eq!(results.len(), 1);
376 assert_eq!(results[0].pid, 22);
377 assert_eq!(results[0].key_type, SshKeyType::Ed25519);
378 assert_eq!(
379 results[0].key_data,
380 "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzBZ"
381 );
382 assert_eq!(results[0].comment, "user@host");
383 }
384
385 #[test]
386 fn extracts_rsa_key_without_comment() {
387 let vaddr: u64 = 0xFFFF_8000_0010_0000;
388 let paddr: u64 = 0x0080_0000;
389 let mut data = vec![0u8; 4096];
390
391 data[0..4].copy_from_slice(&99u32.to_le_bytes());
393 let tasks_addr = vaddr + 16;
394 data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
395 data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
396 data[32..36].copy_from_slice(b"sshd");
397 let mm_addr = vaddr + 0x200;
398 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
399
400 data[0x200..0x208].copy_from_slice(&0x1000u64.to_le_bytes());
402 let vma_addr = vaddr + 0x300;
403 data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes());
404
405 let heap_vaddr: u64 = 0x0000_5555_0000_0000;
407 let heap_paddr: u64 = 0x0090_0000;
408 data[0x300..0x308].copy_from_slice(&heap_vaddr.to_le_bytes());
409 data[0x308..0x310].copy_from_slice(&(heap_vaddr + 0x1000).to_le_bytes());
410 data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes());
411 data[0x318..0x320].copy_from_slice(&0x1u64.to_le_bytes()); data[0x320..0x328].copy_from_slice(&0u64.to_le_bytes());
413 data[0x328..0x330].copy_from_slice(&0u64.to_le_bytes());
414
415 let mut heap = vec![0u8; 4096];
417 let key_line = b"ssh-rsa AAAAB3NzaC1yc2EAAA\n";
418 heap[0x200..0x200 + key_line.len()].copy_from_slice(key_line);
419
420 let reader = make_test_reader(&data, vaddr, paddr, &[(heap_vaddr, heap_paddr, &heap)]);
421 let results = extract_ssh_keys(&reader).unwrap();
422
423 assert_eq!(results.len(), 1);
424 assert_eq!(results[0].pid, 99);
425 assert_eq!(results[0].key_type, SshKeyType::Rsa);
426 assert_eq!(results[0].key_data, "ssh-rsa AAAAB3NzaC1yc2EAAA");
427 assert!(results[0].comment.is_empty());
428 }
429
430 #[test]
431 fn deduplicates_identical_keys() {
432 let vaddr: u64 = 0xFFFF_8000_0010_0000;
433 let paddr: u64 = 0x0080_0000;
434 let mut data = vec![0u8; 4096];
435
436 data[0..4].copy_from_slice(&10u32.to_le_bytes());
438 let tasks_addr = vaddr + 16;
439 data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
440 data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
441 data[32..36].copy_from_slice(b"sshd");
442 let mm_addr = vaddr + 0x200;
443 data[48..56].copy_from_slice(&mm_addr.to_le_bytes());
444
445 data[0x200..0x208].copy_from_slice(&0x1000u64.to_le_bytes());
447 let vma_addr = vaddr + 0x300;
448 data[0x208..0x210].copy_from_slice(&vma_addr.to_le_bytes());
449
450 let heap_vaddr: u64 = 0x0000_5555_0000_0000;
452 let heap_paddr: u64 = 0x0090_0000;
453 data[0x300..0x308].copy_from_slice(&heap_vaddr.to_le_bytes());
454 data[0x308..0x310].copy_from_slice(&(heap_vaddr + 0x1000).to_le_bytes());
455 data[0x310..0x318].copy_from_slice(&0u64.to_le_bytes());
456 data[0x318..0x320].copy_from_slice(&0x1u64.to_le_bytes()); data[0x320..0x328].copy_from_slice(&0u64.to_le_bytes());
458 data[0x328..0x330].copy_from_slice(&0u64.to_le_bytes());
459
460 let mut heap = vec![0u8; 4096];
462 let key_line = b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA root@server\0";
463 heap[0x100..0x100 + key_line.len()].copy_from_slice(key_line);
464 heap[0x300..0x300 + key_line.len()].copy_from_slice(key_line);
465
466 let reader = make_test_reader(&data, vaddr, paddr, &[(heap_vaddr, heap_paddr, &heap)]);
467 let results = extract_ssh_keys(&reader).unwrap();
468
469 assert_eq!(results.len(), 1, "duplicate keys should be deduplicated");
470 }
471
472 #[test]
473 fn parse_key_line_ed25519_with_comment() {
474 let (kt, kd, comment) =
475 parse_key_line("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzBZ user@host").unwrap();
476 assert_eq!(kt, SshKeyType::Ed25519);
477 assert_eq!(kd, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzBZ");
478 assert_eq!(comment, "user@host");
479 }
480
481 #[test]
482 fn parse_key_line_rsa_no_comment() {
483 let (kt, kd, comment) = parse_key_line("ssh-rsa AAAAB3NzaC1yc2EAAA").unwrap();
484 assert_eq!(kt, SshKeyType::Rsa);
485 assert_eq!(kd, "ssh-rsa AAAAB3NzaC1yc2EAAA");
486 assert!(comment.is_empty());
487 }
488
489 #[test]
490 fn parse_key_line_invalid() {
491 assert!(parse_key_line("").is_none());
492 assert!(parse_key_line("not-a-key AAAA").is_none());
493 assert!(parse_key_line("ssh-rsa").is_none()); }
495
496 #[test]
497 fn missing_init_task_symbol() {
498 let isf = IsfBuilder::new()
499 .add_struct("task_struct", 64)
500 .add_field("task_struct", "pid", 0, "int")
501 .add_field("task_struct", "tasks", 8, "list_head")
502 .add_struct("list_head", 16)
503 .add_field("list_head", "next", 0, "pointer")
504 .add_field("list_head", "prev", 8, "pointer")
505 .build_json();
506
507 let resolver = IsfResolver::from_value(&isf).unwrap();
508 let (cr3, mem) = PageTableBuilder::new().build();
509 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
510 let reader = ObjectReader::new(vas, Box::new(resolver));
511
512 let result = extract_ssh_keys(&reader);
513 assert!(
514 matches!(result, Err(crate::Error::MissingKernelSymbol { ref name }) if name == "init_task"),
515 "expected MissingKernelSymbol {{name: \"init_task\"}}, got {result:?}"
516 );
517 }
518
519 #[test]
520 fn missing_tasks_field_returns_missing_field() {
521 let isf = IsfBuilder::new()
522 .add_struct("task_struct", 64)
523 .add_field("task_struct", "pid", 0, "int")
524 .add_struct("list_head", 16)
526 .add_field("list_head", "next", 0, "pointer")
527 .add_field("list_head", "prev", 8, "pointer")
528 .add_symbol("init_task", 0xFFFF_8000_0010_0000)
529 .build_json();
530 let resolver = IsfResolver::from_value(&isf).unwrap();
531 let (cr3, mem) = PageTableBuilder::new().build();
532 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
533 let reader: ObjectReader<SyntheticPhysMem> = ObjectReader::new(vas, Box::new(resolver));
534 let result = extract_ssh_keys(&reader);
535 assert!(
536 matches!(result, Err(crate::Error::MissingField { ref struct_name, ref field_name }) if struct_name == "task_struct" && field_name == "tasks"),
537 "expected MissingField task_struct.tasks, got {result:?}"
538 );
539 }
540}