1pub use winevt_core::binary::{EvtxChunkHeader, IntegrityAnomaly, RECORD_MAGIC};
5
6#[derive(Debug, Clone, serde::Serialize)]
8pub struct MemoryCarvedRecord {
9 pub offset: usize,
11 pub raw: Vec<u8>,
13 pub binxml_valid: bool,
15}
16
17pub fn scan_memory_buffer(buf: &[u8]) -> Vec<MemoryCarvedRecord> {
26 const MIN_RECORD: usize = 28; const MAX_RECORD: usize = 65536;
28 const FRAGMENT_HEADER: u8 = 0x0F;
29
30 if buf.len() < 8 {
31 return vec![];
32 }
33
34 let mut out = Vec::new();
35 let mut pos = 0usize;
36
37 while pos + 8 <= buf.len() {
38 if buf[pos..pos + 4] != RECORD_MAGIC {
39 pos += 1;
40 continue;
41 }
42 let size = u32::from_le_bytes(
43 buf[pos + 4..pos + 8].try_into().unwrap_or([0; 4]),
44 ) as usize;
45 if size < MIN_RECORD || size > MAX_RECORD {
46 pos += 1;
47 continue;
48 }
49 if pos + size > buf.len() {
50 pos += 1;
51 continue;
52 }
53 let raw = buf[pos..pos + size].to_vec();
54 let binxml_valid = raw.len() > 24 && raw[24] == FRAGMENT_HEADER;
56 out.push(MemoryCarvedRecord { offset: pos, raw, binxml_valid });
57 pos += size;
58 }
59 out
60}
61
62#[derive(Debug, Clone, serde::Serialize)]
64pub struct RecoveredEtwEvent {
65 pub timestamp: u64,
66 pub provider_id: String,
67 pub event_id: u16,
68 pub payload: Vec<u8>,
69}
70
71#[derive(Debug, Clone, serde::Serialize)]
73pub struct MemoryRecoveredChunk {
74 pub vaddr: u64,
75 pub header: EvtxChunkHeader,
76 pub record_count: u32,
77 pub first_timestamp: u64,
78 pub last_timestamp: u64,
79 pub channel: String,
80 pub source_process: Option<String>,
81 pub source_pid: Option<u32>,
82 pub anti_forensic: Vec<IntegrityAnomaly>,
83}
84
85#[derive(Debug, Clone, serde::Serialize)]
87pub struct RecoveredEtwSession {
88 pub logger_id: u32,
89 pub name: String,
90 pub is_running: bool,
91 pub buffer_count: u32,
92 pub buffer_size: u32,
93 pub events_lost: u32,
94 pub log_mode: u32,
95 pub buffer_events: Vec<RecoveredEtwEvent>,
96}
97
98#[derive(Debug, Clone, serde::Serialize)]
100pub enum EtwTamperingIndicator {
101 HighEventsLost {
103 session_name: String,
104 events_lost: u32,
105 threshold: u32,
106 },
107 MissingEventLogSession { expected_name: String },
109 SessionStopped { session_name: String },
111 ZeroBuffers { session_name: String },
113 SuspiciousLogMode { session_name: String, log_mode: u32 },
115}
116
117const HIGH_EVENTS_LOST_THRESHOLD: u32 = 1000;
119
120pub fn identify_eventlog_sessions(sessions: &[RecoveredEtwSession]) -> Vec<&RecoveredEtwSession> {
122 sessions
123 .iter()
124 .filter(|s| s.name.starts_with("EventLog-"))
125 .collect()
126}
127
128const REQUIRED_EVENTLOG_SESSIONS: &[&str] = &[
130 "EventLog-Security",
131 "EventLog-System",
132 "EventLog-Application",
133];
134
135pub fn detect_etw_tampering(sessions: &[RecoveredEtwSession]) -> Vec<EtwTamperingIndicator> {
137 let mut indicators = Vec::new();
138
139 for required in REQUIRED_EVENTLOG_SESSIONS {
141 if !sessions.iter().any(|s| s.name == *required) {
142 indicators.push(EtwTamperingIndicator::MissingEventLogSession {
143 expected_name: (*required).to_string(),
144 });
145 }
146 }
147
148 for session in sessions {
149 if session.events_lost > HIGH_EVENTS_LOST_THRESHOLD {
150 indicators.push(EtwTamperingIndicator::HighEventsLost {
151 session_name: session.name.clone(),
152 events_lost: session.events_lost,
153 threshold: HIGH_EVENTS_LOST_THRESHOLD,
154 });
155 }
156 if session.log_mode == 0 {
157 indicators.push(EtwTamperingIndicator::SuspiciousLogMode {
158 session_name: session.name.clone(),
159 log_mode: 0,
160 });
161 }
162 if session.name.starts_with("EventLog-") && !session.is_running {
164 indicators.push(EtwTamperingIndicator::SessionStopped {
165 session_name: session.name.clone(),
166 });
167 }
168 if session.is_running && session.buffer_count == 0 {
170 indicators.push(EtwTamperingIndicator::ZeroBuffers {
171 session_name: session.name.clone(),
172 });
173 }
174 }
175 indicators
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 fn make_chunk_header() -> winevt_core::binary::EvtxChunkHeader {
183 let mut buf = vec![0u8; 0x10000];
184 buf[0..8].copy_from_slice(b"ElfChnk\0");
185 buf[8..16].copy_from_slice(&1u64.to_le_bytes());
186 buf[16..24].copy_from_slice(&10u64.to_le_bytes());
187 buf[24..32].copy_from_slice(&1u64.to_le_bytes());
188 buf[32..40].copy_from_slice(&10u64.to_le_bytes());
189 buf[40..44].copy_from_slice(&0x80u32.to_le_bytes());
190 buf[44..48].copy_from_slice(&0x200u32.to_le_bytes());
191 buf[48..52].copy_from_slice(&0x200u32.to_le_bytes());
192 buf[52..56].copy_from_slice(&0u32.to_le_bytes());
193 let crc = crc32fast::hash(&buf[0..0x78]);
194 buf[0x78..0x7C].copy_from_slice(&crc.to_le_bytes());
195 winevt_core::binary::EvtxChunkHeader::parse(&buf).unwrap()
196 }
197
198 #[test]
199 fn memory_recovered_chunk_can_be_constructed() {
200 let header = make_chunk_header();
201 let chunk = MemoryRecoveredChunk {
202 vaddr: 0xFFFF_C000_0000_0000,
203 header,
204 record_count: 10,
205 first_timestamp: 100,
206 last_timestamp: 200,
207 channel: "Security".to_string(),
208 source_process: Some("EventLog".to_string()),
209 source_pid: Some(1234),
210 anti_forensic: vec![],
211 };
212 assert_eq!(chunk.vaddr, 0xFFFF_C000_0000_0000);
213 assert_eq!(chunk.record_count, 10);
214 assert_eq!(chunk.channel, "Security");
215 }
216
217 #[test]
218 fn recovered_etw_session_can_be_constructed() {
219 let session = RecoveredEtwSession {
220 logger_id: 7,
221 name: "EventLog-Security".to_string(),
222 is_running: true,
223 buffer_count: 4,
224 buffer_size: 64,
225 events_lost: 0,
226 log_mode: 0x00000101,
227 buffer_events: vec![],
228 };
229 assert_eq!(session.logger_id, 7);
230 assert_eq!(session.name, "EventLog-Security");
231 assert!(session.is_running);
232 }
233
234 #[test]
235 fn identify_eventlog_sessions_returns_only_prefixed() {
236 let sessions = vec![
237 RecoveredEtwSession {
238 logger_id: 1,
239 name: "EventLog-Security".to_string(),
240 is_running: true,
241 buffer_count: 4,
242 buffer_size: 64,
243 events_lost: 0,
244 log_mode: 0,
245 buffer_events: vec![],
246 },
247 RecoveredEtwSession {
248 logger_id: 2,
249 name: "NT Kernel Logger".to_string(),
250 is_running: true,
251 buffer_count: 4,
252 buffer_size: 64,
253 events_lost: 0,
254 log_mode: 0,
255 buffer_events: vec![],
256 },
257 RecoveredEtwSession {
258 logger_id: 3,
259 name: "EventLog-System".to_string(),
260 is_running: true,
261 buffer_count: 4,
262 buffer_size: 64,
263 events_lost: 0,
264 log_mode: 0,
265 buffer_events: vec![],
266 },
267 ];
268 let found = identify_eventlog_sessions(&sessions);
269 assert_eq!(found.len(), 2);
270 assert!(found.iter().any(|s| s.name == "EventLog-Security"));
271 assert!(found.iter().any(|s| s.name == "EventLog-System"));
272 }
273
274 #[test]
275 fn identify_eventlog_sessions_returns_empty_when_none_match() {
276 let sessions = vec![RecoveredEtwSession {
277 logger_id: 1,
278 name: "NT Kernel Logger".to_string(),
279 is_running: true,
280 buffer_count: 4,
281 buffer_size: 64,
282 events_lost: 0,
283 log_mode: 0,
284 buffer_events: vec![],
285 }];
286 let found = identify_eventlog_sessions(&sessions);
287 assert!(found.is_empty());
288 }
289
290 #[test]
291 fn detect_etw_tampering_flags_high_events_lost() {
292 let sessions = vec![RecoveredEtwSession {
293 logger_id: 1,
294 name: "EventLog-Security".to_string(),
295 is_running: true,
296 buffer_count: 4,
297 buffer_size: 64,
298 events_lost: 1001,
299 log_mode: 0x00000101,
300 buffer_events: vec![],
301 }];
302 let indicators = detect_etw_tampering(&sessions);
303 let has_high_lost = indicators.iter().any(|ind| {
304 matches!(ind, EtwTamperingIndicator::HighEventsLost { session_name, events_lost, .. }
305 if session_name == "EventLog-Security" && *events_lost == 1001)
306 });
307 assert!(
308 has_high_lost,
309 "expected HighEventsLost indicator, got: {:?}",
310 indicators
311 );
312 }
313
314 #[test]
315 fn detect_etw_tampering_returns_empty_for_low_events_lost() {
316 let sessions = vec![
318 RecoveredEtwSession {
319 logger_id: 1,
320 name: "EventLog-Security".to_string(),
321 is_running: true,
322 buffer_count: 4,
323 buffer_size: 64,
324 events_lost: 100,
325 log_mode: 0x00000101,
326 buffer_events: vec![],
327 },
328 RecoveredEtwSession {
329 logger_id: 2,
330 name: "EventLog-System".to_string(),
331 is_running: true,
332 buffer_count: 4,
333 buffer_size: 64,
334 events_lost: 0,
335 log_mode: 0x00000101,
336 buffer_events: vec![],
337 },
338 RecoveredEtwSession {
339 logger_id: 3,
340 name: "EventLog-Application".to_string(),
341 is_running: true,
342 buffer_count: 4,
343 buffer_size: 64,
344 events_lost: 0,
345 log_mode: 0x00000101,
346 buffer_events: vec![],
347 },
348 ];
349 let indicators = detect_etw_tampering(&sessions);
350 assert!(
351 indicators.is_empty(),
352 "expected empty indicators for low events_lost (all sessions present), got: {:?}",
353 indicators
354 );
355 }
356
357 #[test]
360 fn detect_etw_tampering_flags_suspicious_log_mode_zero() {
361 let sessions = vec![RecoveredEtwSession {
362 logger_id: 1,
363 name: "EventLog-Security".to_string(),
364 is_running: true,
365 buffer_count: 4,
366 buffer_size: 64,
367 events_lost: 0,
368 log_mode: 0, buffer_events: vec![],
370 }];
371 let indicators = detect_etw_tampering(&sessions);
372 let has_suspicious = indicators.iter().any(|ind| {
373 matches!(ind, EtwTamperingIndicator::SuspiciousLogMode { session_name, .. }
374 if session_name == "EventLog-Security")
375 });
376 assert!(
377 has_suspicious,
378 "expected SuspiciousLogMode indicator for log_mode=0, got: {:?}",
379 indicators
380 );
381 }
382
383 #[test]
384 fn detect_etw_tampering_returns_empty_for_normal_active_session() {
385 let sessions = vec![
387 RecoveredEtwSession {
388 logger_id: 1,
389 name: "EventLog-Security".to_string(),
390 is_running: true,
391 buffer_count: 4,
392 buffer_size: 64,
393 events_lost: 5,
394 log_mode: 0x00000101,
395 buffer_events: vec![],
396 },
397 RecoveredEtwSession {
398 logger_id: 2,
399 name: "EventLog-System".to_string(),
400 is_running: true,
401 buffer_count: 4,
402 buffer_size: 64,
403 events_lost: 0,
404 log_mode: 0x00000101,
405 buffer_events: vec![],
406 },
407 RecoveredEtwSession {
408 logger_id: 3,
409 name: "EventLog-Application".to_string(),
410 is_running: true,
411 buffer_count: 4,
412 buffer_size: 64,
413 events_lost: 0,
414 log_mode: 0x00000101,
415 buffer_events: vec![],
416 },
417 ];
418 let indicators = detect_etw_tampering(&sessions);
419 assert!(
420 indicators.is_empty(),
421 "expected empty indicators for normal session (all required present), got: {:?}",
422 indicators
423 );
424 }
425
426 #[test]
427 fn identify_eventlog_sessions_finds_security_system_application() {
428 let sessions = vec![
429 RecoveredEtwSession {
430 logger_id: 1,
431 name: "EventLog-Security".to_string(),
432 is_running: true,
433 buffer_count: 4,
434 buffer_size: 64,
435 events_lost: 0,
436 log_mode: 0,
437 buffer_events: vec![],
438 },
439 RecoveredEtwSession {
440 logger_id: 2,
441 name: "EventLog-System".to_string(),
442 is_running: true,
443 buffer_count: 4,
444 buffer_size: 64,
445 events_lost: 0,
446 log_mode: 0,
447 buffer_events: vec![],
448 },
449 RecoveredEtwSession {
450 logger_id: 3,
451 name: "EventLog-Application".to_string(),
452 is_running: true,
453 buffer_count: 4,
454 buffer_size: 64,
455 events_lost: 0,
456 log_mode: 0,
457 buffer_events: vec![],
458 },
459 RecoveredEtwSession {
460 logger_id: 4,
461 name: "NT Kernel Logger".to_string(),
462 is_running: true,
463 buffer_count: 4,
464 buffer_size: 64,
465 events_lost: 0,
466 log_mode: 0,
467 buffer_events: vec![],
468 },
469 ];
470 let found = identify_eventlog_sessions(&sessions);
471 assert_eq!(found.len(), 3);
472 let names: Vec<&str> = found.iter().map(|s| s.name.as_str()).collect();
473 assert!(names.contains(&"EventLog-Security"));
474 assert!(names.contains(&"EventLog-System"));
475 assert!(names.contains(&"EventLog-Application"));
476 }
477
478 #[test]
479 fn memory_recovered_chunk_implements_serialize() {
480 let header = make_chunk_header();
481 let chunk = MemoryRecoveredChunk {
482 vaddr: 0x1000,
483 header,
484 record_count: 1,
485 first_timestamp: 0,
486 last_timestamp: 0,
487 channel: "Security".to_string(),
488 source_process: None,
489 source_pid: None,
490 anti_forensic: vec![],
491 };
492 let json = serde_json::to_string(&chunk).expect("serialize MemoryRecoveredChunk");
493 assert!(json.contains("Security"));
494 }
495
496 #[test]
497 fn recovered_etw_session_implements_serialize() {
498 let session = RecoveredEtwSession {
499 logger_id: 1,
500 name: "EventLog-Security".to_string(),
501 is_running: true,
502 buffer_count: 4,
503 buffer_size: 64,
504 events_lost: 0,
505 log_mode: 0,
506 buffer_events: vec![],
507 };
508 let json = serde_json::to_string(&session).expect("serialize RecoveredEtwSession");
509 assert!(json.contains("EventLog-Security"));
510 }
511
512 fn make_session(
515 name: &str,
516 is_running: bool,
517 buffer_count: u32,
518 log_mode: u32,
519 ) -> RecoveredEtwSession {
520 RecoveredEtwSession {
521 logger_id: 1,
522 name: name.to_string(),
523 is_running,
524 buffer_count,
525 buffer_size: 64,
526 events_lost: 0,
527 log_mode,
528 buffer_events: vec![],
529 }
530 }
531
532 #[test]
533 fn detect_etw_tampering_all_expected_present_no_new_indicators() {
534 let sessions = vec![
535 make_session("EventLog-Security", true, 4, 0x101),
536 make_session("EventLog-System", true, 4, 0x101),
537 make_session("EventLog-Application", true, 4, 0x101),
538 ];
539 let indicators = detect_etw_tampering(&sessions);
540 let has_missing = indicators
541 .iter()
542 .any(|i| matches!(i, EtwTamperingIndicator::MissingEventLogSession { .. }));
543 let has_stopped = indicators
544 .iter()
545 .any(|i| matches!(i, EtwTamperingIndicator::SessionStopped { .. }));
546 let has_zero = indicators
547 .iter()
548 .any(|i| matches!(i, EtwTamperingIndicator::ZeroBuffers { .. }));
549 assert!(
550 !has_missing && !has_stopped && !has_zero,
551 "expected no missing/stopped/zero indicators, got: {:?}",
552 indicators
553 );
554 }
555
556 #[test]
557 fn detect_etw_tampering_missing_security_emits_missing_indicator() {
558 let sessions = vec![
559 make_session("EventLog-System", true, 4, 0x101),
560 make_session("EventLog-Application", true, 4, 0x101),
561 ];
562 let indicators = detect_etw_tampering(&sessions);
563 let has_missing = indicators.iter().any(|i| {
564 matches!(i, EtwTamperingIndicator::MissingEventLogSession { expected_name }
565 if expected_name == "EventLog-Security")
566 });
567 assert!(
568 has_missing,
569 "expected MissingEventLogSession for EventLog-Security, got: {:?}",
570 indicators
571 );
572 }
573
574 #[test]
575 fn detect_etw_tampering_stopped_session_emits_stopped_indicator() {
576 let sessions = vec![
577 make_session("EventLog-Security", false, 4, 0x101),
578 make_session("EventLog-System", true, 4, 0x101),
579 make_session("EventLog-Application", true, 4, 0x101),
580 ];
581 let indicators = detect_etw_tampering(&sessions);
582 let has_stopped = indicators.iter().any(|i| {
583 matches!(i, EtwTamperingIndicator::SessionStopped { session_name }
584 if session_name == "EventLog-Security")
585 });
586 assert!(
587 has_stopped,
588 "expected SessionStopped for EventLog-Security, got: {:?}",
589 indicators
590 );
591 }
592
593 #[test]
594 fn detect_etw_tampering_zero_buffers_running_emits_zero_indicator() {
595 let sessions = vec![
596 make_session("EventLog-Security", true, 0, 0x101),
597 make_session("EventLog-System", true, 4, 0x101),
598 make_session("EventLog-Application", true, 4, 0x101),
599 ];
600 let indicators = detect_etw_tampering(&sessions);
601 let has_zero = indicators.iter().any(|i| {
602 matches!(i, EtwTamperingIndicator::ZeroBuffers { session_name }
603 if session_name == "EventLog-Security")
604 });
605 assert!(
606 has_zero,
607 "expected ZeroBuffers for EventLog-Security (running, 0 buffers), got: {:?}",
608 indicators
609 );
610 }
611
612 #[test]
613 fn detect_etw_tampering_all_three_missing_when_no_sessions() {
614 let sessions: Vec<RecoveredEtwSession> = vec![];
615 let indicators = detect_etw_tampering(&sessions);
616 let missing_names: Vec<&str> = indicators
617 .iter()
618 .filter_map(|i| {
619 if let EtwTamperingIndicator::MissingEventLogSession { expected_name } = i {
620 Some(expected_name.as_str())
621 } else {
622 None
623 }
624 })
625 .collect();
626 assert!(missing_names.contains(&"EventLog-Security"));
627 assert!(missing_names.contains(&"EventLog-System"));
628 assert!(missing_names.contains(&"EventLog-Application"));
629 }
630
631 fn make_raw_record(record_id: u64, ts: u64, payload: &[u8]) -> Vec<u8> {
634 let size = (4 + 4 + 8 + 8 + payload.len() + 4) as u32;
635 let mut rec = vec![0u8; size as usize];
636 rec[0..4].copy_from_slice(&[0x2A, 0x2A, 0x00, 0x00]);
637 rec[4..8].copy_from_slice(&size.to_le_bytes());
638 rec[8..16].copy_from_slice(&record_id.to_le_bytes());
639 rec[16..24].copy_from_slice(&ts.to_le_bytes());
640 rec[24..24 + payload.len()].copy_from_slice(payload);
641 let tail = size as usize - 4;
642 rec[tail..].copy_from_slice(&size.to_le_bytes());
643 rec
644 }
645
646 fn valid_binxml_payload() -> Vec<u8> {
647 vec![0x0Fu8, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]
649 }
650
651 #[test]
652 fn scan_empty_buffer_returns_empty() {
653 let records = scan_memory_buffer(&[]);
654 assert!(records.is_empty(), "empty buffer must return empty");
655 }
656
657 #[test]
658 fn scan_buffer_with_no_magic_returns_empty() {
659 let buf = vec![0xFFu8; 256];
660 let records = scan_memory_buffer(&buf);
661 assert!(records.is_empty(), "buffer with no record magic must return empty");
662 }
663
664 #[test]
665 fn scan_buffer_with_injected_record_finds_it() {
666 let payload = valid_binxml_payload();
667 let rec = make_raw_record(42, 100_000, &payload);
668 let mut buf = vec![0u8; 64];
669 buf.extend_from_slice(&rec);
670 buf.extend_from_slice(&[0u8; 64]);
671 let records = scan_memory_buffer(&buf);
672 assert_eq!(records.len(), 1, "should find exactly one record; got {}", records.len());
673 assert_eq!(records[0].offset, 64, "record should be at offset 64");
674 }
675
676 #[test]
677 fn scan_ignores_random_bytes_matching_magic_only() {
678 let mut buf = vec![0u8; 128];
680 buf[10] = 0x2A;
681 buf[11] = 0x2A;
682 buf[12] = 0x00;
683 buf[13] = 0x00;
684 let records = scan_memory_buffer(&buf);
686 assert!(
687 records.is_empty(),
688 "magic with invalid size must be ignored; got {} records", records.len()
689 );
690 }
691
692 #[test]
693 fn scan_multi_record_buffer_finds_all() {
694 let payload = valid_binxml_payload();
695 let rec1 = make_raw_record(1, 100, &payload);
696 let rec2 = make_raw_record(2, 200, &payload);
697 let rec3 = make_raw_record(3, 300, &payload);
698 let mut buf = vec![0u8; 32];
699 buf.extend_from_slice(&rec1);
700 buf.extend_from_slice(&rec2);
701 buf.extend_from_slice(&rec3);
702 let records = scan_memory_buffer(&buf);
703 assert_eq!(records.len(), 3, "should find 3 records; got {}", records.len());
704 assert_eq!(records[0].offset, 32);
705 assert!(records[1].offset > records[0].offset);
706 assert!(records[2].offset > records[1].offset);
707 }
708
709 #[test]
710 fn scan_record_beyond_buffer_end_is_ignored() {
711 let mut buf = vec![0x2A, 0x2A, 0x00, 0x00];
713 buf.extend_from_slice(&65000u32.to_le_bytes()); buf.extend_from_slice(&[0u8; 20]);
715 let records = scan_memory_buffer(&buf);
716 assert!(
717 records.is_empty(),
718 "record extending beyond buffer must be ignored"
719 );
720 }
721}