use crate::err::{
ChunkError, DeserializationError, DeserializationResult, EvtxChunkResult, EvtxError,
};
use crate::evtx_record::{EVTX_RECORD_HEADER_SIZE, EvtxRecord, EvtxRecordHeader};
use crate::utils::bytes;
use log::{debug, info, trace};
use std::io::Cursor;
use crate::binxml::ir::{IrTemplateCache, build_tree_from_binxml_bytes_direct};
use crate::string_cache::StringCache;
use crate::{ParserSettings, checksum_ieee};
use bumpalo::Bump;
use std::sync::Arc;
const EVTX_CHUNK_HEADER_SIZE: usize = 512;
bitflags! {
#[derive(Debug)]
pub struct ChunkFlags: u32 {
const EMPTY = 0x0;
const DIRTY = 0x1;
const NO_CRC32 = 0x4;
}
}
#[derive(Debug)]
pub struct EvtxChunkHeader {
pub first_event_record_number: u64,
pub last_event_record_number: u64,
pub first_event_record_id: u64,
pub last_event_record_id: u64,
pub header_size: u32,
pub last_event_record_data_offset: u32,
pub free_space_offset: u32,
pub events_checksum: u32,
pub header_chunk_checksum: u32,
pub flags: ChunkFlags,
strings_offsets: Vec<u32>,
template_offsets: Vec<u32>,
}
pub struct EvtxChunkData {
pub header: EvtxChunkHeader,
pub data: Vec<u8>,
}
impl EvtxChunkData {
pub fn new(data: Vec<u8>, validate_checksum: bool) -> EvtxChunkResult<Self> {
let header = EvtxChunkHeader::from_bytes(&data)?;
let chunk = EvtxChunkData { header, data };
if validate_checksum && !chunk.validate_checksum() {
return Err(ChunkError::InvalidChunkChecksum {
expected: 0,
found: 0,
});
}
Ok(chunk)
}
pub fn parse(&mut self, settings: Arc<ParserSettings>) -> EvtxChunkResult<EvtxChunk<'_>> {
EvtxChunk::new(&self.data, &self.header, Arc::clone(&settings))
}
pub fn parse_with_arena(
&mut self,
settings: Arc<ParserSettings>,
arena: Bump,
) -> EvtxChunkResult<EvtxChunk<'_>> {
EvtxChunk::new_with_arena(&self.data, &self.header, Arc::clone(&settings), arena)
}
pub fn validate_data_checksum(&self) -> bool {
debug!("Validating data checksum");
let checksum_disabled = self.header.flags.contains(ChunkFlags::NO_CRC32);
let expected_checksum = if !checksum_disabled {
self.header.events_checksum
} else {
0
};
let computed_checksum = if !checksum_disabled {
checksum_ieee(
&self.data[EVTX_CHUNK_HEADER_SIZE..self.header.free_space_offset as usize],
)
} else {
0
};
debug!(
"Expected checksum: {:?}, found: {:?}",
expected_checksum, computed_checksum
);
computed_checksum == expected_checksum
}
pub fn validate_header_checksum(&self) -> bool {
debug!("Validating header checksum");
let checksum_disabled = self.header.flags.contains(ChunkFlags::NO_CRC32);
let expected_checksum = if !checksum_disabled {
self.header.header_chunk_checksum
} else {
0
};
let header_bytes_1 = &self.data[..120];
let header_bytes_2 = &self.data[128..512];
let bytes_for_checksum: Vec<u8> = header_bytes_1
.iter()
.chain(header_bytes_2)
.cloned()
.collect();
let computed_checksum = if !checksum_disabled {
checksum_ieee(bytes_for_checksum.as_slice())
} else {
0
};
debug!(
"Expected checksum: {:?}, found: {:?}",
expected_checksum, computed_checksum
);
computed_checksum == expected_checksum
}
pub fn validate_checksum(&self) -> bool {
self.validate_header_checksum() && self.validate_data_checksum()
}
}
#[derive(Debug)]
pub struct EvtxChunk<'chunk> {
pub data: &'chunk [u8],
pub header: &'chunk EvtxChunkHeader,
pub string_cache: StringCache,
pub arena: Bump,
pub settings: Arc<ParserSettings>,
}
impl<'chunk> EvtxChunk<'chunk> {
pub fn new(
data: &'chunk [u8],
header: &'chunk EvtxChunkHeader,
settings: Arc<ParserSettings>,
) -> EvtxChunkResult<EvtxChunk<'chunk>> {
EvtxChunk::new_with_arena(data, header, settings, Bump::new())
}
pub fn new_with_arena(
data: &'chunk [u8],
header: &'chunk EvtxChunkHeader,
settings: Arc<ParserSettings>,
mut arena: Bump,
) -> EvtxChunkResult<EvtxChunk<'chunk>> {
arena.reset();
let _cursor = Cursor::new(data);
info!("Initializing string cache");
let string_cache = StringCache::populate(data, &header.strings_offsets)
.map_err(|e| ChunkError::FailedToBuildStringCache { source: e })?;
Ok(EvtxChunk {
header,
data,
string_cache,
arena,
settings,
})
}
pub fn into_arena(self) -> Bump {
self.arena
}
pub fn iter(&mut self) -> IterChunkRecords<'_> {
let estimated_template_buckets = self
.header
.template_offsets
.iter()
.filter(|&&offset| offset > 0)
.count();
IterChunkRecords {
settings: Arc::clone(&self.settings),
chunk: self,
offset_from_chunk_start: EVTX_CHUNK_HEADER_SIZE as u64,
exhausted: false,
ir_template_cache: IrTemplateCache::with_capacity(
estimated_template_buckets,
&self.arena,
),
}
}
}
pub struct IterChunkRecords<'a> {
chunk: &'a EvtxChunk<'a>,
offset_from_chunk_start: u64,
exhausted: bool,
settings: Arc<ParserSettings>,
ir_template_cache: IrTemplateCache<'a>,
}
impl<'a> Iterator for IterChunkRecords<'a> {
type Item = std::result::Result<EvtxRecord<'a>, EvtxError>;
fn next(&mut self) -> Option<<Self as Iterator>::Item> {
let effective_free_space_offset = u64::from(self.chunk.header.free_space_offset)
.min(self.chunk.data.len().try_into().unwrap_or(u64::MAX));
if self.exhausted || self.offset_from_chunk_start >= effective_free_space_offset {
return None;
}
let record_start = self.offset_from_chunk_start;
let record_start_usize = record_start as usize;
if record_start_usize >= self.chunk.data.len() {
self.exhausted = true;
return None;
}
if self.chunk.data.len() - record_start_usize < 4 {
self.exhausted = true;
return None;
}
let record_header =
match EvtxRecordHeader::from_bytes_at(self.chunk.data, record_start_usize) {
Ok(record_header) => record_header,
Err(DeserializationError::InvalidEvtxRecordHeaderMagic { magic }) => {
if magic == [0, 0, 0, 0] {
self.exhausted = true;
return None;
}
self.exhausted = true;
return Some(Err(EvtxError::DeserializationError(
DeserializationError::InvalidEvtxRecordHeaderMagic { magic },
)));
}
Err(DeserializationError::Truncated { .. }) => {
self.exhausted = true;
return None;
}
Err(err) => {
self.exhausted = true;
return Some(Err(EvtxError::DeserializationError(err)));
}
};
info!("Record id - {}", record_header.event_record_id);
debug!("Record header - {:?}", record_header);
let binxml_data_size = match record_header.record_data_size() {
Ok(size) => size,
Err(err) => {
self.exhausted = true;
return Some(Err(err));
}
};
trace!("Need to deserialize {} bytes of binxml", binxml_data_size);
let binxml_start = record_start + EVTX_RECORD_HEADER_SIZE as u64;
let binxml_end = binxml_start.saturating_add(binxml_data_size as u64);
if binxml_end as usize > self.chunk.data.len() {
self.exhausted = true;
return Some(Err(EvtxError::FailedToParseRecord {
record_id: record_header.event_record_id,
source: Box::new(EvtxError::FailedToCreateRecordModel(
"record BinXML slice is out of bounds",
)),
}));
}
let bytes = &self.chunk.data[binxml_start as usize..binxml_end as usize];
let tree_result =
build_tree_from_binxml_bytes_direct(bytes, self.chunk, &mut self.ir_template_cache)
.map_err(|err| EvtxError::FailedToParseRecord {
record_id: record_header.event_record_id,
source: Box::new(err),
});
self.offset_from_chunk_start += u64::from(record_header.data_size);
if self.chunk.header.last_event_record_id == record_header.event_record_id {
self.exhausted = true;
}
let tree = match tree_result {
Ok(tree) => tree,
Err(err) => return Some(Err(err)),
};
Some(Ok(EvtxRecord {
chunk: self.chunk,
event_record_id: record_header.event_record_id,
timestamp: record_header.timestamp,
tree,
binxml_offset: record_start + EVTX_RECORD_HEADER_SIZE as u64,
binxml_size: binxml_data_size,
settings: Arc::clone(&self.settings),
}))
}
}
impl EvtxChunkHeader {
pub fn from_bytes(data: &[u8]) -> DeserializationResult<EvtxChunkHeader> {
let _ = bytes::slice_r(data, 0, EVTX_CHUNK_HEADER_SIZE, "EVTX chunk header")?;
let magic = bytes::read_array_r::<8>(data, 0, "chunk header magic")?;
if &magic != b"ElfChnk\x00" {
return Err(DeserializationError::InvalidEvtxChunkMagic { magic });
}
let first_event_record_number =
bytes::read_u64_le_r(data, 8, "chunk.first_event_record_number")?;
let last_event_record_number =
bytes::read_u64_le_r(data, 16, "chunk.last_event_record_number")?;
let first_event_record_id = bytes::read_u64_le_r(data, 24, "chunk.first_event_record_id")?;
let last_event_record_id = bytes::read_u64_le_r(data, 32, "chunk.last_event_record_id")?;
let header_size = bytes::read_u32_le_r(data, 40, "chunk.header_size")?;
let last_event_record_data_offset =
bytes::read_u32_le_r(data, 44, "chunk.last_event_record_data_offset")?;
let free_space_offset = bytes::read_u32_le_r(data, 48, "chunk.free_space_offset")?;
let events_checksum = bytes::read_u32_le_r(data, 52, "chunk.events_checksum")?;
let raw_flags = bytes::read_u32_le_r(data, 120, "chunk.flags")?;
let flags = ChunkFlags::from_bits_truncate(raw_flags);
let header_chunk_checksum = bytes::read_u32_le_r(data, 124, "chunk.header_chunk_checksum")?;
let strings_offsets = bytes::read_u32_vec_le_r(data, 128, 64, "chunk.strings_offsets")?;
let template_offsets = bytes::read_u32_vec_le_r(data, 384, 32, "chunk.template_offsets")?;
Ok(EvtxChunkHeader {
first_event_record_number,
last_event_record_number,
first_event_record_id,
last_event_record_id,
header_size,
last_event_record_data_offset,
free_space_offset,
events_checksum,
header_chunk_checksum,
flags,
template_offsets,
strings_offsets,
})
}
pub fn from_reader(input: &mut Cursor<&[u8]>) -> DeserializationResult<EvtxChunkHeader> {
let start = input.position() as usize;
let buf = input.get_ref();
let slice = bytes::slice_r(buf, start, EVTX_CHUNK_HEADER_SIZE, "EVTX chunk header")?;
let header = Self::from_bytes(slice)?;
input.set_position((start + EVTX_CHUNK_HEADER_SIZE) as u64);
Ok(header)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ensure_env_logger_initialized;
use crate::evtx_parser::EVTX_CHUNK_SIZE;
use crate::evtx_parser::EVTX_FILE_HEADER_SIZE;
use std::io::Cursor;
#[test]
fn test_parses_evtx_chunk_header() {
ensure_env_logger_initialized();
let evtx_file = include_bytes!("../samples/security.evtx");
let chunk_header =
&evtx_file[EVTX_FILE_HEADER_SIZE..EVTX_FILE_HEADER_SIZE + EVTX_CHUNK_HEADER_SIZE];
let mut cursor = Cursor::new(chunk_header);
let chunk_header = EvtxChunkHeader::from_reader(&mut cursor).unwrap();
let expected = EvtxChunkHeader {
first_event_record_number: 1,
last_event_record_number: 91,
first_event_record_id: 1,
last_event_record_id: 91,
header_size: 128,
last_event_record_data_offset: 64928,
free_space_offset: 65376,
events_checksum: 4_252_479_141,
header_chunk_checksum: 978_805_790,
flags: ChunkFlags::EMPTY,
strings_offsets: vec![0_u32; 64],
template_offsets: vec![0_u32; 32],
};
assert_eq!(
chunk_header.first_event_record_number,
expected.first_event_record_number
);
assert_eq!(
chunk_header.last_event_record_number,
expected.last_event_record_number
);
assert_eq!(
chunk_header.first_event_record_id,
expected.first_event_record_id
);
assert_eq!(
chunk_header.last_event_record_id,
expected.last_event_record_id
);
assert_eq!(chunk_header.header_size, expected.header_size);
assert_eq!(
chunk_header.last_event_record_data_offset,
expected.last_event_record_data_offset
);
assert_eq!(chunk_header.free_space_offset, expected.free_space_offset);
assert_eq!(chunk_header.events_checksum, expected.events_checksum);
assert_eq!(
chunk_header.header_chunk_checksum,
expected.header_chunk_checksum
);
assert!(!chunk_header.strings_offsets.is_empty());
assert!(!chunk_header.template_offsets.is_empty());
}
#[test]
fn test_validate_checksum() {
ensure_env_logger_initialized();
let evtx_file = include_bytes!("../samples/security.evtx");
let chunk_data =
evtx_file[EVTX_FILE_HEADER_SIZE..EVTX_FILE_HEADER_SIZE + EVTX_CHUNK_SIZE].to_vec();
let chunk = EvtxChunkData::new(chunk_data, false).unwrap();
assert!(chunk.validate_checksum());
}
#[test]
fn test_iter_ends_cleanly_when_chunk_header_offsets_are_too_large() {
ensure_env_logger_initialized();
let evtx_file = include_bytes!("../samples/security.evtx");
let chunk_data =
evtx_file[EVTX_FILE_HEADER_SIZE..EVTX_FILE_HEADER_SIZE + EVTX_CHUNK_SIZE].to_vec();
let mut baseline = EvtxChunkData::new(chunk_data.clone(), false).unwrap();
let settings = Arc::new(ParserSettings::new());
let baseline_count = {
let mut chunk = baseline.parse(Arc::clone(&settings)).unwrap();
chunk
.iter()
.try_fold(0usize, |acc, record| record.map(|_| acc + 1))
.unwrap()
};
let mut corrupted = EvtxChunkData::new(chunk_data, false).unwrap();
corrupted.header.last_event_record_id =
corrupted.header.last_event_record_id.saturating_add(100);
corrupted.header.free_space_offset = EVTX_CHUNK_SIZE as u32;
let corrupted_count = {
let mut chunk = corrupted.parse(settings).unwrap();
chunk
.iter()
.try_fold(0usize, |acc, record| record.map(|_| acc + 1))
.unwrap()
};
assert_eq!(corrupted_count, baseline_count);
}
}