Skip to content

Commit 51206fa

Browse files
authored
Fix parsing when chunk header offsets are too large (#273)
Treat all-zero record header magic as end-of-chunk slack to avoid failing on incorrect free_space_offset/last_event_record_id (issue #197). Adds a regression test and updates dirty-sample expected error counts.
1 parent 23a98bc commit 51206fa

3 files changed

Lines changed: 79 additions & 11 deletions

File tree

src/evtx_chunk.rs

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,20 +239,51 @@ impl<'a> Iterator for IterChunkRecords<'a> {
239239
type Item = std::result::Result<EvtxRecord<'a>, EvtxError>;
240240

241241
fn next(&mut self) -> Option<<Self as Iterator>::Item> {
242-
if self.exhausted
243-
|| self.offset_from_chunk_start >= u64::from(self.chunk.header.free_space_offset)
244-
{
242+
// Be resilient to corrupted chunk headers: `free_space_offset` is user-controlled data
243+
// coming from the EVTX stream, and may point past the end of the chunk.
244+
let effective_free_space_offset = u64::from(self.chunk.header.free_space_offset)
245+
.min(self.chunk.data.len().try_into().unwrap_or(u64::MAX));
246+
247+
if self.exhausted || self.offset_from_chunk_start >= effective_free_space_offset {
248+
return None;
249+
}
250+
251+
let start = self.offset_from_chunk_start as usize;
252+
if start >= self.chunk.data.len() {
253+
// Avoid panicking on an out-of-bounds slice if the header is corrupted.
254+
self.exhausted = true;
255+
return None;
256+
}
257+
258+
let remaining = &self.chunk.data[start..];
259+
if remaining.len() < 4 {
260+
// Not enough bytes for the record header magic, treat as end-of-chunk.
261+
self.exhausted = true;
245262
return None;
246263
}
247264

248-
let mut cursor = Cursor::new(&self.chunk.data[self.offset_from_chunk_start as usize..]);
265+
let mut cursor = Cursor::new(remaining);
249266

250267
let record_header = match EvtxRecordHeader::from_reader(&mut cursor) {
251268
Ok(record_header) => record_header,
269+
Err(DeserializationError::InvalidEvtxRecordHeaderMagic { magic }) => {
270+
// Some producers write incorrect `free_space_offset` / `last_event_record_id`.
271+
// In such cases we may attempt to parse the chunk slack area, which is typically
272+
// zero-padded. Treat an all-zero "magic" as a clean end-of-chunk instead of
273+
// emitting an error (see issue #197).
274+
if magic == [0, 0, 0, 0] {
275+
self.exhausted = true;
276+
return None;
277+
}
278+
279+
self.exhausted = true;
280+
return Some(Err(EvtxError::DeserializationError(
281+
DeserializationError::InvalidEvtxRecordHeaderMagic { magic },
282+
)));
283+
}
252284
Err(err) => {
253285
// We currently do not try to recover after an invalid record.
254286
self.exhausted = true;
255-
256287
return Some(Err(EvtxError::DeserializationError(err)));
257288
}
258289
};
@@ -449,4 +480,41 @@ mod tests {
449480
let chunk = EvtxChunkData::new(chunk_data, false).unwrap();
450481
assert!(chunk.validate_checksum());
451482
}
483+
484+
#[test]
485+
fn test_iter_ends_cleanly_when_chunk_header_offsets_are_too_large() {
486+
ensure_env_logger_initialized();
487+
488+
let evtx_file = include_bytes!("../samples/security.evtx");
489+
let chunk_data =
490+
evtx_file[EVTX_FILE_HEADER_SIZE..EVTX_FILE_HEADER_SIZE + EVTX_CHUNK_SIZE].to_vec();
491+
492+
// Parse once to get a baseline count.
493+
let mut baseline = EvtxChunkData::new(chunk_data.clone(), false).unwrap();
494+
let settings = Arc::new(ParserSettings::new());
495+
let baseline_count = {
496+
let mut chunk = baseline.parse(Arc::clone(&settings)).unwrap();
497+
chunk
498+
.iter()
499+
.try_fold(0usize, |acc, record| record.map(|_| acc + 1))
500+
.unwrap()
501+
};
502+
503+
// Now simulate a broken chunk header like in issue #197: `last_event_record_id` and
504+
// `free_space_offset` are larger than the actual number of records/data.
505+
let mut corrupted = EvtxChunkData::new(chunk_data, false).unwrap();
506+
corrupted.header.last_event_record_id =
507+
corrupted.header.last_event_record_id.saturating_add(100);
508+
corrupted.header.free_space_offset = EVTX_CHUNK_SIZE as u32;
509+
510+
let corrupted_count = {
511+
let mut chunk = corrupted.parse(settings).unwrap();
512+
chunk
513+
.iter()
514+
.try_fold(0usize, |acc, record| record.map(|_| acc + 1))
515+
.unwrap()
516+
};
517+
518+
assert_eq!(corrupted_count, baseline_count);
519+
}
452520
}

tests/test_full_samples.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,19 +128,19 @@ fn test_dirty_sample_with_a_chunk_past_zeros() {
128128

129129
#[test]
130130
fn test_dirty_sample_with_a_bad_chunk_magic() {
131-
test_full_sample(sample_with_a_bad_chunk_magic(), 270, 5)
131+
test_full_sample(sample_with_a_bad_chunk_magic(), 270, 2)
132132
}
133133

134134
#[test]
135135
fn test_dirty_sample_binxml_with_incomplete_token() {
136136
// Contains an unparsable record
137-
test_full_sample(sample_binxml_with_incomplete_sid(), 6, 1)
137+
test_full_sample(sample_binxml_with_incomplete_sid(), 6, 0)
138138
}
139139

140140
#[test]
141141
fn test_dirty_sample_binxml_with_incomplete_template() {
142142
// Contains an unparsable record
143-
test_full_sample(sample_binxml_with_incomplete_template(), 17, 1)
143+
test_full_sample(sample_binxml_with_incomplete_template(), 17, 0)
144144
}
145145

146146
#[test]

tests/test_full_samples_streaming.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,19 +151,19 @@ fn test_dirty_sample_with_a_chunk_past_zeros_streaming() {
151151

152152
#[test]
153153
fn test_dirty_sample_with_a_bad_chunk_magic_streaming() {
154-
test_full_sample_streaming(sample_with_a_bad_chunk_magic(), 270, 5)
154+
test_full_sample_streaming(sample_with_a_bad_chunk_magic(), 270, 2)
155155
}
156156

157157
#[test]
158158
fn test_dirty_sample_binxml_with_incomplete_token_streaming() {
159159
// Contains an unparsable record
160-
test_full_sample_streaming(sample_binxml_with_incomplete_sid(), 6, 1)
160+
test_full_sample_streaming(sample_binxml_with_incomplete_sid(), 6, 0)
161161
}
162162

163163
#[test]
164164
fn test_dirty_sample_binxml_with_incomplete_template_streaming() {
165165
// Contains an unparsable record
166-
test_full_sample_streaming(sample_binxml_with_incomplete_template(), 17, 1)
166+
test_full_sample_streaming(sample_binxml_with_incomplete_template(), 17, 0)
167167
}
168168

169169
#[test]

0 commit comments

Comments
 (0)