diff --git a/.gitignore b/.gitignore index 5cabcd17..1e93fd2a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,9 @@ profile/* binaries/* benchmarks/* .PRE_PATH + +# Local-only fixtures (e.g. PE/CRIM blobs for WEVT_TEMPLATE work). Keep out of git. +samples_local/ + +# Local vendor checkouts (for format research / patching upstream). +external/ diff --git a/Cargo.lock b/Cargo.lock index d10329e5..5275d7bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -557,6 +557,8 @@ dependencies = [ "dialoguer", "encoding", "env_logger", + "glob", + "goblin", "hashbrown", "indoc", "insta", @@ -616,6 +618,17 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "goblin" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4db6758c546e6f81f265638c980e5e84dfbda80cfd8e89e02f83454c8e8124bd" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "half" version = "2.4.1" @@ -864,6 +877,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plotters" version = "0.3.7" @@ -1092,6 +1111,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scroll" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "semver" version = "1.0.25" diff --git a/Cargo.toml b/Cargo.toml index 31cd5deb..821f16f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,10 @@ thiserror = "2" log = { version = "0.4.17", features = ["release_max_level_debug"] } winstructs = "0.3.0" hashbrown = { version = "^0.15", features = ["inline-more"] } +# Optional: PE parsing for `evtx_dump extract-wevt-templates` resource extraction. +goblin = { version = "0.10", optional = true } +# Optional: used by `evtx_dump extract-wevt-templates` for cross-platform glob expansion. +glob = { version = "0.3", optional = true } # Optional for multithreading. rayon = { version = "1", optional = true } @@ -49,8 +53,10 @@ rpmalloc = { version = "0.2.2", optional = true } [features] default = ["multithreading", "evtx_dump"] fast-alloc = ["tikv-jemallocator", "rpmalloc"] -evtx_dump = ["simplelog", "clap", "dialoguer", "indoc", "anyhow", "tempfile"] +evtx_dump = ["simplelog", "clap", "dialoguer", "indoc", "anyhow", "tempfile", "wevt_templates"] multithreading = ["rayon"] +# Enable WEVT_TEMPLATE extraction helpers (used by `evtx_dump extract-wevt-templates`). +wevt_templates = ["glob", "goblin"] [dev-dependencies] insta = { version = "1", features = ["json"] } diff --git a/README.md b/README.md index ead45064..2494676f 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,25 @@ Some examples To force single threaded usage (which will also ensure order), `-t 1` can be passed. +## Offline template rendering (WEVT_TEMPLATE) + +EVTX records can reference template definitions stored in provider binaries (EXE/DLL/SYS). `evtx_dump` can extract those templates into an offline cache and use them at render time. + +**Note:** this functionality requires building `evtx_dump` with the Cargo feature `wevt_templates` (release binaries may already include it). + +- Build a cache (writes extracted blobs under `/tmp/wevt_cache/` and emits an index JSONL on stdout): + - `evtx_dump extract-wevt-templates --input --output-dir /tmp/wevt_cache --overwrite > /tmp/wevt_cache/index.jsonl` +- Dump an EVTX file while using the cache (deterministic rule: only applies when a record fails due to an explicit missing/corrupt template GUID): + - `evtx_dump --wevt-cache-index /tmp/wevt_cache/index.jsonl ` + +Debugging helpers: +- Dump a record’s `TemplateInstance` substitution values (JSONL): + - `evtx_dump dump-template-instances --input --record-id | head -n1` +- Render a specific template GUID with substitutions (XML to stdout): + - `evtx_dump apply-wevt-cache --cache-index /tmp/wevt_cache/index.jsonl --template-guid --evtx --record-id ` + +See [`docs/wevt_templates.md`](docs/wevt_templates.md) for details and background (issue #103). + ## Example usage (as library): ```rust use evtx::EvtxParser; diff --git a/docs/wevt_templates.md b/docs/wevt_templates.md new file mode 100644 index 00000000..58da5f1b --- /dev/null +++ b/docs/wevt_templates.md @@ -0,0 +1,230 @@ +# WEVT_TEMPLATE extraction: an offline template cache for rendering “template-less” EVTX records + +## The problem (offline template cache for template-less records) + +Windows EVTX records often **don’t carry a complete, self-contained “format string”** for their XML. Instead, a record can reference a **template definition** that lives elsewhere: + +- On-disk EVTX files store record payloads as **BinXML**. +- The “shape” of the XML (element names, attribute names, substitution slots) can be defined in **provider templates** that are embedded in Windows binaries (EXE/DLL/SYS) as `WEVT_TEMPLATE` resources. + +This becomes painful when: + +- You carve records out of slack/unallocated space (no reliable access to the original provider binaries). +- You want to render logs **offline** on a system that doesn’t have the original provider manifests installed. + +So the goal is to build an offline template cache: + +1. Extract all `WEVT_TEMPLATE` resources from a corpus of binaries. +2. Parse them into templates and **stable join keys**. +3. Use those templates later to render records that are missing template metadata. + +This is exactly what [issue #103](https://github.com/omerbenamram/evtx/issues/103) was about. + +## What this repo implements + +This repo adds: + +- A **production-grade, deterministic** parser for the `WEVT_TEMPLATE` payload format: + - `CRIM` (manifest) → provider directory → `WEVT` provider element directory → elements like `EVNT`/`TTBL`/`TEMP` + - aligned to the `libfwevt` documentation and behavior +- A BinXML mode for `WEVT_TEMPLATE` templates: + - **WEVT inline-name encoding** (names stored inline, not via chunk string tables) + - **strict MS-EVEN6 NameHash validation** +- An `evtx_dump` subcommand that can extract templates from many binaries: + - supports `--input` (multi), `--glob` (multi), `--recursive` + - emits JSONL for downstream processing (“cache without committing to a DB”) + +All of this ships under the optional Cargo feature **`wevt_templates`** to keep default builds lean. + +## Where templates live: PE resources → WEVT_TEMPLATE → CRIM + +Provider templates are embedded as a PE resource type `WEVT_TEMPLATE`. The resource data blob typically starts with `CRIM...` and contains: + +- A CRIM header (version, provider count) +- An array of provider descriptors pointing at provider blocks +- Each provider has a `WEVT` header with an element descriptor directory +- Elements include: + - `EVNT`: event definitions (this is where the canonical `template_offset` join lives) + - `TTBL`: template table containing `TEMP` template definitions + - `CHAN`, `KEYW`, `LEVL`, `OPCO`, `TASK`, `MAPS` (metadata tables) + +The key observation (mirroring `libfwevt`) is: + +> In `EVNT`, each event definition includes a `template_offset` (relative to CRIM) that points directly to a `TEMP` definition. + +## TTBL/TEMP: templates + substitution items + +Inside `TTBL`, templates are stored as a sequence of `TEMP` entries: + +- TEMP header includes: + - descriptor counts (`item_descriptor_count`, `item_name_count`) + - `template_items_offset` (relative to CRIM) + - template GUID (identifier) + - BinXML fragment immediately after the header +- Template item descriptors are stored *outside* the BinXML fragment (at `template_items_offset`) + - each descriptor describes a substitution slot (type/count/length) and points to a UTF-16 name + +This is how we can render useful placeholders: + +- `TEMP` gives the XML “shape” (element names and substitution tokens) +- item descriptors/names give semantic names for `{sub:N}` such as `{sub:0:Foo}` + +## BinXML dialect: EVTX chunk vs WEVT inline names + +EVTX record BinXML typically resolves element/attribute names via chunk string tables (offset-based references). + +In `WEVT_TEMPLATE` payloads, BinXML uses a different encoding: + +- Names are stored inline as: + - `u16 NameHash` + - `u16 NameNumChars` + - `UTF-16LE chars` + - `u16 NUL` + +We implement this as a separate name encoding mode (internally `WevtInline`) and enforce the MS-EVEN6 NameHash. + +### NameHash (strict) + +NameHash is computed over UTF-16 code units: + +``` +hash = 0 +for each u16 code_unit in name: + hash = hash * 65599 + code_unit +stored_hash = low_16_bits(hash) +``` + +If `stored_hash` doesn’t match, parsing fails (by design; “no best-effort”). + +## The join keys (how to actually use this offline) + +There are two practical joins: + +1. **Template GUID** (strong, stable):\n + - EVTX template definitions carry a GUID\n + - WEVT `TEMP` templates carry the same GUID\n + - If you have an EVTX record that exposes the template GUID (e.g. from a `TemplateInstance`), this is the cleanest join. + +2. **Provider event → template_offset → template GUID**:\n + - `EVNT` event definition includes `template_offset`.\n + - Resolve it to a `TEMP` at that offset.\n + - You now have the template GUID (and the full template definition). + +The CLI emits these joins so you can build a simple offline cache index without inventing a database format. + +## End-to-end: build a cache and use it + +### 1) Build the cache (extract templates from binaries) + +Build/run the CLI with the feature enabled: + +```bash +cargo run --release --features wevt_templates --bin evtx_dump -- \ + extract-wevt-templates --help +``` + +Example using the public `services.exe` sample (stored as a `.gif` in this repo): + +```bash +cargo run --release --features wevt_templates --bin evtx_dump -- \ + extract-wevt-templates \ + --input samples_local/services.exe.gif \ + --output-dir /tmp/wevt_cache \ + --overwrite \ + --split-ttbl \ + --dump-temp-xml \ + --dump-events \ + --dump-items \ + > /tmp/wevt_cache/index.jsonl +``` + +What you get: + +- `/tmp/wevt_cache/*.bin`: raw `WEVT_TEMPLATE` resource blobs (CRIM payloads) +- `/tmp/wevt_cache/temp/*.bin`: raw `TEMP` slices +- `/tmp/wevt_cache/temp_xml/*.xml`: rendered template XML skeletons +- `/tmp/wevt_cache/index.jsonl`: JSONL describing resources, events, template GUIDs, and template items + +### 2) Look up a template GUID for an event (offline join) + +Assuming you know: + +- `provider_guid` (from the record’s ``) +- `event_id` and `version` (from the record’s `` and version field) + +You can find the template GUID from the JSONL: + +```bash +jq -r ' + select(has("provider_guid")) | + select(.provider_guid=="{PROVIDER_GUID}" and .event_id=={EVENT_ID} and .version=={VERSION}) | + .template_guid +' /tmp/wevt_cache/index.jsonl | head -n1 +``` + +Then locate the corresponding rendered template XML skeleton: + +```bash +jq -r ' + select(has("output_path") and (.output_path|endswith(".xml"))) | + select(.guid=="{TEMPLATE_GUID}") | + .output_path +' /tmp/wevt_cache/index.jsonl | head -n1 +``` + +### 3) Apply it to a carved record (what remains) + +The cache solves the hard part: **offline extraction and parsing of provider templates**, plus **stable joins**. + +To fully render a carved record end-to-end you still need the record’s **substitution values array** (the `{sub:N}` values). Once you have those values (from the record’s BinXML TemplateInstance data), you can: + +- pick the template (by GUID or by event→template join) +- substitute `{sub:N(:Name)?}` slots with actual values (with proper escaping) + +This last “apply substitutions” step is not yet wired as a single CLI command, but the format pieces are now in place to build it cleanly without heuristics. + +## Implementation map (where to read the code) + +- Template extraction + CLI wiring:\n + - `src/bin/evtx_dump.rs` (subcommand `extract-wevt-templates`)\n + - `src/wevt_templates/mod.rs` (public API + re-exports)\n + - `src/wevt_templates/extract.rs` (PE resource extraction)\n + - `src/wevt_templates/binxml.rs` (WEVT inline-name BinXML parsing helpers)\n + - `src/wevt_templates/render.rs` (XML rendering helpers)\n + - `src/wevt_templates/temp.rs` (TTBL/TEMP discovery helpers)\n +- Spec-backed manifest parsing:\n + - `src/wevt_templates/manifest/mod.rs` (module entrypoint)\n + - `src/wevt_templates/manifest/types.rs` (CRIM/WEVT/EVNT/TTBL/TEMP types)\n + - `src/wevt_templates/manifest/parse.rs` (spec-backed parsing)\n + - `src/wevt_templates/manifest/error.rs` (parse error types)\n +- BinXML dialect support:\n + - `src/binxml/name.rs` (WEVT inline-name parsing + strict NameHash)\n + - `src/binxml/deserializer.rs` (threading `BinXmlNameEncoding` through token parsing)\n + +## Testing strategy + +We avoid shipping proprietary Windows binaries: + +- Committed minimal synthetic PE fixture for `WEVT_TEMPLATE` extraction. +- Synthetic CRIM/WEVT/TTBL/TEMP blobs for structural correctness + join tests. +- Ignored integration test against the `services.exe` sample if present locally (or downloaded by the test when enabled). + +## Future work + +If we want truly end-to-end “render carved record using cache”, the missing piece is a small API/CLI that: + +1. parses a record’s TemplateInstance substitution array +2. resolves template GUID via cache +3. applies substitution values to the template definition + +There’s also room to expand parsing of `MAPS` (e.g. `BMAP`) if/when the format is fully nailed down. + +## References (primary sources) + +- Issue #103 (original feature gap / motivation): `https://github.com/omerbenamram/evtx/issues/103`\n +- MS-EVEN6 BinXml (inline name format + NameHash algorithm): `https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-even6/c73573ae-1c90-43a2-a65f-ad7501155956`\n +- libfwevt (manifest format + reference implementation): `https://github.com/libyal/libfwevt`\n +- libfwevt manifest spec doc (CRIM/WEVT/EVNT/TTBL/TEMP tables): `https://github.com/libyal/libfwevt/blob/main/documentation/Windows%20Event%20manifest%20binary%20format.asciidoc`\n +- libevtx (EVTX format reference): `https://github.com/libyal/libevtx/blob/main/documentation/Windows%20XML%20Event%20Log%20(EVTX).asciidoc` + + diff --git a/src/bin/evtx_dump.rs b/src/bin/evtx_dump.rs index 32c7331c..8044c94a 100644 --- a/src/bin/evtx_dump.rs +++ b/src/bin/evtx_dump.rs @@ -17,6 +17,13 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use tempfile::tempfile; +#[path = "evtx_dump/apply_wevt_cache.rs"] +mod apply_wevt_cache; +#[path = "evtx_dump/dump_template_instances.rs"] +mod dump_template_instances; +#[path = "evtx_dump/extract_wevt_templates.rs"] +mod extract_wevt_templates; + #[cfg(all(not(target_env = "msvc"), feature = "fast-alloc"))] use tikv_jemallocator::Jemalloc; @@ -53,6 +60,8 @@ struct EvtxDump { stop_after_error: bool, /// When set, only the specified events (offseted reltaive to file) will be outputted. ranges: Option, + #[cfg(feature = "wevt_templates")] + wevt_cache: Option>, } impl EvtxDump { @@ -166,6 +175,12 @@ impl EvtxDump { Box::new(BufWriter::new(io::stdout())) }; + #[cfg(feature = "wevt_templates")] + let wevt_cache = matches + .get_one::("wevt-cache-index") + .map(|p| evtx::wevt_templates::WevtCache::load(p).map(std::sync::Arc::new)) + .transpose()?; + Ok(EvtxDump { parser_settings: ParserSettings::new() .num_threads(num_threads.try_into().expect("u32 -> usize")) @@ -181,6 +196,8 @@ impl EvtxDump { verbosity_level, stop_after_error, ranges: event_ranges, + #[cfg(feature = "wevt_templates")] + wevt_cache, }) } @@ -196,6 +213,22 @@ impl EvtxDump { match self.output_format { EvtxOutputFormat::XML => { + #[cfg(feature = "wevt_templates")] + if let Some(cache) = self.wevt_cache.clone() { + let iter = parser.serialized_records(move |record_res| { + record_res + .and_then(|record| record.into_xml_with_wevt_cache(cache.as_ref())) + }); + for record in iter { + self.dump_record(record)? + } + } else { + for record in parser.records() { + self.dump_record(record)? + } + } + + #[cfg(not(feature = "wevt_templates"))] for record in parser.records() { self.dump_record(record)? } @@ -203,11 +236,45 @@ impl EvtxDump { EvtxOutputFormat::JSON => { match self.json_parser { JsonParserKind::Streaming => { + #[cfg(feature = "wevt_templates")] + if let Some(cache) = self.wevt_cache.clone() { + let iter = parser.serialized_records(move |record_res| { + record_res.and_then(|record| { + record.into_json_stream_with_wevt_cache(cache.as_ref()) + }) + }); + for record in iter { + self.dump_record(record)? + } + } else { + for record in parser.records_json_stream() { + self.dump_record(record)? + } + } + + #[cfg(not(feature = "wevt_templates"))] for record in parser.records_json_stream() { self.dump_record(record)? } } JsonParserKind::Legacy => { + #[cfg(feature = "wevt_templates")] + if let Some(cache) = self.wevt_cache.clone() { + let iter = parser.serialized_records(move |record_res| { + record_res.and_then(|record| { + record.into_json_with_wevt_cache(cache.as_ref()) + }) + }); + for record in iter { + self.dump_record(record)? + } + } else { + for record in parser.records_json() { + self.dump_record(record)? + } + } + + #[cfg(not(feature = "wevt_templates"))] for record in parser.records_json() { self.dump_record(record)? } @@ -421,14 +488,14 @@ fn main() -> Result<()> { .map(|e| e.name()) .collect::>(); - let matches = Command::new("EVTX Parser") + let cmd = Command::new("EVTX Parser") .version(env!("CARGO_PKG_VERSION")) .author("Omer B. ") .about("Utility to parse EVTX files") .arg( Arg::new("INPUT") - .required(true) - .help("Input EVTX file path, or '-' to read from stdin."), + .required(false) + .help("Input EVTX file path, or '-' to read from stdin. Required unless using a subcommand."), ) .arg( Arg::new("num-threads") @@ -516,7 +583,19 @@ fn main() -> Result<()> { .value_parser(all_encoings) .default_value(encoding::all::WINDOWS_1252.name()) .help("When set, controls the codec of ansi encoded strings the file."), - ) + ); + + // Optional: when provided, use an offline WEVT template cache as a fallback for records + // whose embedded EVTX templates are missing/corrupt (common in carved/dirty logs). + #[cfg(feature = "wevt_templates")] + let cmd = cmd.arg( + Arg::new("wevt-cache-index") + .long("wevt-cache-index") + .value_name("INDEX_JSONL") + .help("Path to a WEVT template cache index JSONL (from `extract-wevt-templates`). When set, evtx_dump will try to render records using this cache if the embedded EVTX template expansion fails."), + ); + + let matches = cmd .arg( Arg::new("stop-after-one-error") .long("stop-after-one-error") @@ -532,7 +611,27 @@ fn main() -> Result<()> { -vv - debug -vvv - trace NOTE: trace output is only available in debug builds, as it is extremely verbose."#)) - ).get_matches(); + ) + .subcommand(extract_wevt_templates::command()) + .subcommand(dump_template_instances::command()) + .subcommand(apply_wevt_cache::command()) + .get_matches(); + + if let Some(("extract-wevt-templates", sub_matches)) = matches.subcommand() { + return extract_wevt_templates::run(sub_matches); + } + + if let Some(("dump-template-instances", sub_matches)) = matches.subcommand() { + return dump_template_instances::run(sub_matches); + } + + if let Some(("apply-wevt-cache", sub_matches)) = matches.subcommand() { + return apply_wevt_cache::run(sub_matches); + } + + if matches.get_one::("INPUT").is_none() { + bail!("Missing INPUT. Provide an EVTX file path, or use a subcommand (try `--help`)."); + } EvtxDump::from_cli_matches(&matches)?.run()?; diff --git a/src/bin/evtx_dump/apply_wevt_cache.rs b/src/bin/evtx_dump/apply_wevt_cache.rs new file mode 100644 index 00000000..f87ef6ac --- /dev/null +++ b/src/bin/evtx_dump/apply_wevt_cache.rs @@ -0,0 +1,449 @@ +use anyhow::{Context, Result, bail, format_err}; +use clap::{Arg, ArgMatches, Command}; +use indoc::indoc; + +pub fn command() -> Command { + Command::new("apply-wevt-cache") + .about("Render a WEVT template using an offline cache + substitution values") + .long_about(indoc!(r#" + Render a WEVT template using an offline cache + substitution values. + + Inputs: + - A cache index JSONL (stdout from `extract-wevt-templates`). + - A template selector: either --template-guid, or (provider_guid,event_id,version). + - Substitution values: either extracted from an EVTX record (--evtx + --record-id), + or provided as a JSON array (--substitutions / --substitutions-file). + "#)) + .arg( + Arg::new("cache-index") + .long("cache-index") + .required(true) + .value_name("PATH") + .help("Path to cache index JSONL (stdout from `extract-wevt-templates`)."), + ) + .arg( + Arg::new("template-guid") + .long("template-guid") + .value_name("GUID") + .help("Template GUID to render."), + ) + .arg( + Arg::new("provider-guid") + .long("provider-guid") + .value_name("GUID") + .help("Provider GUID (used to resolve template GUID via the cache index)."), + ) + .arg( + Arg::new("event-id") + .long("event-id") + .value_parser(clap::value_parser!(u16).range(0..)) + .value_name("ID") + .help("Event ID (used to resolve template GUID via the cache index)."), + ) + .arg( + Arg::new("version") + .long("version") + .value_parser(clap::value_parser!(u8).range(0..)) + .value_name("V") + .help("Event version (used to resolve template GUID via the cache index)."), + ) + .arg( + Arg::new("evtx") + .long("evtx") + .value_name("PATH") + .help("EVTX file to extract substitution values from (TemplateInstance)."), + ) + .arg( + Arg::new("record-id") + .long("record-id") + .value_parser(clap::value_parser!(u64).range(0..)) + .value_name("ID") + .help("Event record id to extract substitution values from."), + ) + .arg( + Arg::new("template-instance-index") + .long("template-instance-index") + .value_parser(clap::value_parser!(usize)) + .default_value("0") + .value_name("N") + .help("When a record contains multiple TemplateInstance tokens, select which one to use (default: 0)."), + ) + .arg( + Arg::new("substitutions") + .long("substitutions") + .value_name("JSON") + .help("Substitution values as a JSON array (strings/numbers)."), + ) + .arg( + Arg::new("substitutions-file") + .long("substitutions-file") + .value_name("PATH") + .help("Path to a JSON file containing a substitution values array."), + ) + .arg( + Arg::new("output") + .long("output") + .short('o') + .value_name("PATH") + .help("Write rendered XML to this path (default: stdout)."), + ) +} + +pub fn run(matches: &ArgMatches) -> Result<()> { + #[cfg(feature = "wevt_templates")] + { + run_impl(matches) + } + + #[cfg(not(feature = "wevt_templates"))] + { + let _ = matches; + bail!( + "This subcommand requires building `evtx_dump` with template support enabled.\n\ + Example:\n\ + cargo run --bin evtx_dump -- apply-wevt-cache ..." + ); + } +} + +#[cfg(feature = "wevt_templates")] +mod imp { + use super::*; + use evtx::EvtxParser; + use evtx::ParserSettings; + use evtx::binxml::value_variant::BinXmlValue; + use evtx::model::deserialized::BinXMLDeserializedTokens; + use evtx::wevt_templates::manifest::CrimManifest; + use evtx::wevt_templates::render_template_definition_to_xml_with_substitution_values; + use serde_json::Value as JsonValue; + use std::fs; + use std::path::{Path, PathBuf}; + + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + struct ResourceKey { + source: String, + resource: String, + lang_id: u32, + } + + #[derive(Debug, Default)] + struct CacheIndex { + crim_paths: Vec, + event_to_template_guid: std::collections::HashMap<(String, u16, u8), String>, + } + + fn normalize_guid(s: &str) -> String { + evtx::wevt_templates::normalize_guid(s) + } + + fn parse_resource_id(v: &JsonValue) -> Option { + match v { + JsonValue::Number(n) => n.as_u64().map(|id| format!("id:{id}")), + JsonValue::String(s) => Some(format!("name:{s}")), + _ => None, + } + } + + fn load_cache_index(path: &Path) -> Result { + let text = fs::read_to_string(path) + .with_context(|| format!("failed to read cache index `{}`", path.display()))?; + let mut out = CacheIndex::default(); + + // Also map (source,resource,lang) -> CRIM output path. + let mut crim_by_key: std::collections::HashMap = + std::collections::HashMap::new(); + + fn resolve_output_path(index_path: &Path, output_path: &str) -> String { + let p = Path::new(output_path); + if p.is_absolute() { + return output_path.to_string(); + } + let base = index_path.parent().unwrap_or_else(|| Path::new(".")); + base.join(p).to_string_lossy().to_string() + } + + for (line_no, line) in text.lines().enumerate() { + let line = line.trim(); + if line.is_empty() { + continue; + } + let v: JsonValue = serde_json::from_str(line) + .with_context(|| format!("invalid JSONL at {}:{}", path.display(), line_no + 1))?; + + let source = v + .get("source") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let resource = v.get("resource").and_then(parse_resource_id); + let lang_id = v + .get("lang_id") + .and_then(|v| v.as_u64()) + .and_then(|n| u32::try_from(n).ok()); + + // ExtractWevtTemplatesOutputLine: has output_path + size, but no guid/provider_guid/template_guid. + if v.get("output_path").and_then(|p| p.as_str()).is_some() + && v.get("size").is_some() + && v.get("guid").is_none() + && v.get("provider_guid").is_none() + && v.get("template_guid").is_none() + { + if let (Some(source), Some(resource), Some(lang_id)) = (source, resource, lang_id) { + let key = ResourceKey { + source, + resource, + lang_id, + }; + if let Some(p) = v.get("output_path").and_then(|p| p.as_str()) { + crim_by_key.insert(key, resolve_output_path(path, p)); + } + } + continue; + } + + // ExtractWevtEventOutputLine: has provider_guid/event_id/version/template_guid. + let template_guid = v + .get("template_guid") + .and_then(|v| v.as_str()) + .filter(|s| !s.is_empty()); + + if let (Some(provider_guid), Some(event_id), Some(version), Some(template_guid)) = ( + v.get("provider_guid").and_then(|v| v.as_str()), + v.get("event_id") + .and_then(|v| v.as_u64()) + .and_then(|n| u16::try_from(n).ok()), + v.get("version") + .and_then(|v| v.as_u64()) + .and_then(|n| u8::try_from(n).ok()), + template_guid, + ) { + out.event_to_template_guid.insert( + (normalize_guid(provider_guid), event_id, version), + normalize_guid(template_guid), + ); + } + } + + out.crim_paths = crim_by_key.values().cloned().collect(); + Ok(out) + } + + fn value_to_string_lossy(value: &BinXmlValue<'_>) -> String { + match value { + BinXmlValue::EvtHandle => String::new(), + BinXmlValue::BinXmlType(_) => String::new(), + BinXmlValue::EvtXml => String::new(), + _ => value.as_cow_str().into_owned(), + } + } + + fn substitutions_from_evtx( + evtx_path: &Path, + record_id: u64, + template_instance_index: usize, + ) -> Result> { + let settings = ParserSettings::default(); + let mut parser = EvtxParser::from_path(evtx_path) + .with_context(|| format!("Failed to open evtx file at: {}", evtx_path.display()))? + .with_configuration(settings.clone()); + + for chunk_res in parser.chunks() { + let mut chunk_data = chunk_res?; + let mut chunk = chunk_data.parse(std::sync::Arc::new(settings.clone()))?; + for record_res in chunk.iter() { + let record = record_res?; + if record.event_record_id != record_id { + continue; + } + + let mut instances = vec![]; + for t in &record.tokens { + if let BinXMLDeserializedTokens::TemplateInstance(tpl) = t { + instances.push(tpl); + } + } + + let tpl = instances.get(template_instance_index).ok_or_else(|| { + format_err!( + "record {record_id} has no TemplateInstance at index {template_instance_index}" + ) + })?; + + let mut out = Vec::with_capacity(tpl.substitution_array.len()); + for s in &tpl.substitution_array { + match s { + BinXMLDeserializedTokens::Value(v) => out.push(value_to_string_lossy(v)), + _ => out.push(String::new()), + } + } + return Ok(out); + } + } + + bail!("record_id {record_id} not found in {}", evtx_path.display()); + } + + fn substitutions_from_json_array(v: &JsonValue) -> Result> { + let Some(arr) = v.as_array() else { + bail!("substitutions JSON must be an array"); + }; + Ok(arr + .iter() + .map(|v| match v { + JsonValue::Null => String::new(), + JsonValue::String(s) => s.clone(), + JsonValue::Number(n) => n.to_string(), + JsonValue::Bool(b) => b.to_string(), + other => other.to_string(), + }) + .collect()) + } + + pub(super) fn run_impl(matches: &ArgMatches) -> Result<()> { + let cache_index_path = + PathBuf::from(matches.get_one::("cache-index").expect("required")); + let cache = load_cache_index(&cache_index_path)?; + + // Resolve substitutions. + let template_instance_index: usize = *matches + .get_one::("template-instance-index") + .expect("has default"); + + let substitutions = if let (Some(evtx_path), Some(record_id)) = ( + matches.get_one::("evtx").map(PathBuf::from), + matches.get_one::("record-id").copied(), + ) { + substitutions_from_evtx(&evtx_path, record_id, template_instance_index)? + } else if let Some(s) = matches.get_one::("substitutions") { + let v: JsonValue = + serde_json::from_str(s).context("failed to parse --substitutions as JSON")?; + substitutions_from_json_array(&v)? + } else if let Some(p) = matches.get_one::("substitutions-file") { + let text = fs::read_to_string(p) + .with_context(|| format!("failed to read substitutions file `{p}`"))?; + let v: JsonValue = serde_json::from_str(&text) + .context("failed to parse substitutions file as JSON")?; + substitutions_from_json_array(&v)? + } else { + bail!( + "Must provide substitutions via --evtx+--record-id or --substitutions/--substitutions-file" + ); + }; + + // Resolve template guid. + let template_guid = if let Some(g) = matches.get_one::("template-guid") { + normalize_guid(g) + } else if let (Some(provider_guid), Some(event_id), Some(version)) = ( + matches.get_one::("provider-guid"), + matches.get_one::("event-id").copied(), + matches.get_one::("version").copied(), + ) { + let key = (normalize_guid(provider_guid), event_id, version); + cache + .event_to_template_guid + .get(&key) + .cloned() + .ok_or_else(|| { + format_err!( + "no template_guid found in cache index for provider_guid={provider_guid} event_id={event_id} version={version}" + ) + })? + } else { + bail!( + "Must provide either --template-guid or (--provider-guid, --event-id, --version)" + ); + }; + + // Find the template definition in one of the CRIM blobs and render. + let mut rendered: Option = None; + for crim_path in &cache.crim_paths { + let bytes = match fs::read(crim_path) { + Ok(b) => b, + Err(_) => continue, + }; + let manifest = match CrimManifest::parse(&bytes) { + Ok(m) => m, + Err(_) => continue, + }; + + for provider in &manifest.providers { + if let Some(ttbl) = provider.wevt.elements.templates.as_ref() { + for tpl in &ttbl.templates { + if normalize_guid(&tpl.guid.to_string()) == template_guid { + let xml = render_template_definition_to_xml_with_substitution_values( + tpl, + &substitutions, + encoding::all::WINDOWS_1252, + )?; + rendered = Some(xml); + break; + } + } + } + if rendered.is_some() { + break; + } + } + if rendered.is_some() { + break; + } + } + + let xml = rendered.ok_or_else(|| { + format_err!( + "template GUID `{}` not found in any CRIM blobs referenced by `{}`", + template_guid, + cache_index_path.display() + ) + })?; + + if let Some(out_path) = matches.get_one::("output") { + fs::write(out_path, xml.as_bytes()) + .with_context(|| format!("failed to write output `{out_path}`"))?; + } else { + print!("{xml}"); + } + + Ok(()) + } + + #[cfg(test)] + mod tests { + use super::*; + use std::io::Write; + + #[test] + fn normalize_guid_strips_braces_and_is_case_insensitive() { + let braced = "{12345678-1234-1234-1234-123456789ABC}"; + let unbraced = "12345678-1234-1234-1234-123456789abc"; + + assert_eq!(normalize_guid(braced), unbraced); + assert_eq!(normalize_guid(unbraced), unbraced); + } + + #[test] + fn load_cache_index_normalizes_provider_and_template_guids() -> Result<()> { + let mut f = tempfile::NamedTempFile::new().context("tempfile")?; + writeln!( + f, + r#"{{"provider_guid":"{{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}}","event_id":1,"version":2,"template_guid":"{{BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB}}"}}"# + ) + .context("write jsonl")?; + + let cache = load_cache_index(f.path()).context("load_cache_index")?; + + let key = ( + normalize_guid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), + 1u16, + 2u8, + ); + assert_eq!( + cache.event_to_template_guid.get(&key).map(|s| s.as_str()), + Some("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") + ); + Ok(()) + } + } +} + +#[cfg(feature = "wevt_templates")] +use imp::run_impl; diff --git a/src/bin/evtx_dump/dump_template_instances.rs b/src/bin/evtx_dump/dump_template_instances.rs new file mode 100644 index 00000000..90a5b9d6 --- /dev/null +++ b/src/bin/evtx_dump/dump_template_instances.rs @@ -0,0 +1,200 @@ +use anyhow::{Context, Result}; +use clap::{Arg, ArgMatches, Command}; +use indoc::indoc; + +pub fn command() -> Command { + Command::new("dump-template-instances") + .about("Dump BinXML TemplateInstance substitution arrays from EVTX records (JSONL)") + .long_about(indoc!( + r#" + Dump BinXML TemplateInstance substitution arrays from EVTX records as JSONL. + + This is useful for offline rendering workflows where you have a template cache + (from `extract-wevt-templates`) and need the record's substitution values array. + "# + )) + .arg( + Arg::new("input") + .long("input") + .short('i') + .required(true) + .value_name("EVTX") + .help("Input EVTX file path."), + ) + .arg( + Arg::new("record-id") + .long("record-id") + .value_parser(clap::value_parser!(u64).range(0..)) + .value_name("ID") + .help("Only dump template instances for the specified event record id."), + ) + .arg( + Arg::new("template-instance-index") + .long("template-instance-index") + .value_parser(clap::value_parser!(usize)) + .default_value("0") + .value_name("N") + .help("When a record contains multiple TemplateInstance tokens, select which one to dump (default: 0)."), + ) +} + +pub fn run(matches: &ArgMatches) -> Result<()> { + #[cfg(feature = "wevt_templates")] + { + run_impl(matches) + } + + #[cfg(not(feature = "wevt_templates"))] + { + let _ = matches; + anyhow::bail!( + "This subcommand requires building `evtx_dump` with template support enabled.\n\ + Example:\n\ + cargo run --bin evtx_dump -- dump-template-instances ..." + ); + } +} + +#[cfg(feature = "wevt_templates")] +mod imp { + use super::*; + use evtx::{EvtxParser, ParserSettings}; + use serde::Serialize; + use serde_json::Value as JsonValue; + use std::path::PathBuf; + + #[derive(Debug, Serialize)] + struct DumpTemplateInstanceOutputLine { + source: String, + record_id: u64, + timestamp: String, + template_instance_index: usize, + template_id: u32, + template_def_offset: u32, + template_guid: Option, + substitutions: Vec, + } + + fn binxml_value_to_json_lossy( + value: &evtx::binxml::value_variant::BinXmlValue<'_>, + ) -> JsonValue { + use evtx::binxml::value_variant::BinXmlValue; + match value { + BinXmlValue::EvtHandle => JsonValue::Object( + [( + "type".to_string(), + JsonValue::String("EvtHandle".to_string()), + )] + .into_iter() + .collect(), + ), + BinXmlValue::BinXmlType(_) => JsonValue::Object( + [( + "type".to_string(), + JsonValue::String("BinXmlType".to_string()), + )] + .into_iter() + .collect(), + ), + BinXmlValue::EvtXml => JsonValue::Object( + [("type".to_string(), JsonValue::String("EvtXml".to_string()))] + .into_iter() + .collect(), + ), + other => JsonValue::from(other), + } + } + + pub(super) fn run_impl(matches: &ArgMatches) -> Result<()> { + use evtx::model::deserialized::BinXMLDeserializedTokens; + + let input = PathBuf::from(matches.get_one::("input").expect("required")); + let record_id_filter = matches.get_one::("record-id").copied(); + let template_instance_index: usize = *matches + .get_one::("template-instance-index") + .expect("has default"); + + let settings = ParserSettings::default(); + let mut parser = EvtxParser::from_path(&input) + .with_context(|| format!("Failed to open evtx file at: {}", input.display()))? + .with_configuration(settings.clone()); + + let source = input.to_string_lossy().to_string(); + + for chunk_res in parser.chunks() { + let mut chunk_data = match chunk_res { + Ok(c) => c, + Err(e) => { + eprintln!("{e}"); + continue; + } + }; + + let mut chunk = match chunk_data.parse(std::sync::Arc::new(settings.clone())) { + Ok(c) => c, + Err(e) => { + eprintln!("{e}"); + continue; + } + }; + + for record_res in chunk.iter() { + let record = match record_res { + Ok(r) => r, + Err(e) => { + eprintln!("{e}"); + continue; + } + }; + + if record_id_filter.is_some_and(|want| record.event_record_id != want) { + continue; + } + + let mut instances = vec![]; + for t in &record.tokens { + if let BinXMLDeserializedTokens::TemplateInstance(tpl) = t { + instances.push(tpl); + } + } + + let Some(tpl) = instances.get(template_instance_index) else { + continue; + }; + + let mut substitutions = Vec::with_capacity(tpl.substitution_array.len()); + for s in &tpl.substitution_array { + match s { + BinXMLDeserializedTokens::Value(v) => { + substitutions.push(binxml_value_to_json_lossy(v)) + } + other => substitutions.push(JsonValue::String(format!("{other:?}"))), + } + } + + let line = DumpTemplateInstanceOutputLine { + source: source.clone(), + record_id: record.event_record_id, + timestamp: record.timestamp.to_rfc3339(), + template_instance_index, + template_id: tpl.template_id, + template_def_offset: tpl.template_def_offset, + template_guid: tpl.template_guid.as_ref().map(|g| g.to_string()), + substitutions, + }; + + println!("{}", serde_json::to_string(&line)?); + + if record_id_filter.is_some() { + // Found the record we wanted; keep going anyway in case there are duplicates? No. + return Ok(()); + } + } + } + + Ok(()) + } +} + +#[cfg(feature = "wevt_templates")] +use imp::run_impl; diff --git a/src/bin/evtx_dump/extract_wevt_templates.rs b/src/bin/evtx_dump/extract_wevt_templates.rs new file mode 100644 index 00000000..6ee728b6 --- /dev/null +++ b/src/bin/evtx_dump/extract_wevt_templates.rs @@ -0,0 +1,687 @@ +use anyhow::{Context, Result, bail}; +use clap::{Arg, ArgAction, ArgMatches, Command}; +use indoc::indoc; + +pub fn command() -> Command { + let cmd = Command::new("extract-wevt-templates") + .about("Extract WEVT_TEMPLATE resources from PE files (EXE/DLL)") + .long_about(indoc!(r#" + Extract WEVT_TEMPLATE resources from PE files (EXE/DLL). + + This is intended to support building an offline cache of EVTX templates + (see issue #103), without committing to any database format yet. + + Note: this subcommand is included in the default `evtx_dump` build. + "#)) + .arg( + Arg::new("input") + .long("input") + .short('i') + .action(ArgAction::Append) + .value_name("PATH") + .help("Input PE path (file or directory). Can be passed multiple times."), + ) + .arg( + Arg::new("glob") + .long("glob") + .action(ArgAction::Append) + .value_name("PATTERN") + .help("Glob pattern to expand into input paths (cross-platform). Can be passed multiple times."), + ) + .arg( + Arg::new("recursive") + .long("recursive") + .short('r') + .action(ArgAction::SetTrue) + .help("When an input path is a directory (or a glob matches a directory), recurse into it."), + ) + .arg( + Arg::new("extensions") + .long("extensions") + .value_name("EXTS") + .default_value("exe,dll,sys") + .help("Comma-separated list of allowed file extensions when walking directories (default: exe,dll,sys)."), + ) + .arg( + Arg::new("output-dir") + .long("output-dir") + .short('o') + .required(true) + .value_name("DIR") + .help("Directory to write extracted resources into."), + ) + .arg( + Arg::new("overwrite") + .long("overwrite") + .action(ArgAction::SetTrue) + .help("Overwrite output files if they already exist."), + ); + + #[cfg(feature = "wevt_templates")] + let cmd = cmd + .arg( + Arg::new("split-ttbl") + .long("split-ttbl") + .action(ArgAction::SetTrue) + .help("Also split extracted WEVT_TEMPLATE blobs into TTBL/TEMP entries and write each TEMP to /temp/."), + ) + .arg( + Arg::new("dump-temp-xml") + .long("dump-temp-xml") + .action(ArgAction::SetTrue) + .help("Also render each TEMP BinXML fragment to XML and write to /temp_xml/."), + ) + .arg( + Arg::new("dump-events") + .long("dump-events") + .action(ArgAction::SetTrue) + .help("Dump EVNT event definitions (including template_offset join keys) as JSONL."), + ) + .arg( + Arg::new("dump-items") + .long("dump-items") + .action(ArgAction::SetTrue) + .help("Dump TEMP template item descriptors/names as JSONL."), + ); + + cmd +} + +pub fn run(matches: &ArgMatches) -> Result<()> { + #[cfg(feature = "wevt_templates")] + { + run_impl(matches) + } + + #[cfg(not(feature = "wevt_templates"))] + { + let _ = matches; + bail!( + "This subcommand requires building `evtx_dump` with template support enabled.\n\ + Example:\n\ + cargo run --bin evtx_dump -- extract-wevt-templates ..." + ); + } +} + +#[cfg(feature = "wevt_templates")] +mod imp { + use super::*; + use serde::Serialize; + use std::fs; + use std::path::{Path, PathBuf}; + + #[derive(Debug, Clone, Serialize)] + #[serde(untagged)] + enum ResourceIdJson { + Id(u32), + Name(String), + } + + #[derive(Debug, Serialize)] + struct ExtractWevtTemplatesOutputLine { + source: String, + resource: ResourceIdJson, + lang_id: u32, + output_path: String, + size: usize, + } + + #[derive(Debug, Serialize)] + struct ExtractWevtTempOutputLine { + source: String, + resource: ResourceIdJson, + lang_id: u32, + ttbl_offset: u32, + temp_offset: u32, + temp_size: u32, + item_descriptor_count: u32, + item_name_count: u32, + template_items_offset: u32, + event_type: u32, + guid: String, + output_path: String, + } + + #[derive(Debug, Serialize)] + struct ExtractWevtTempXmlOutputLine { + source: String, + resource: ResourceIdJson, + lang_id: u32, + temp_index: usize, + guid: String, + output_path: String, + } + + #[derive(Debug, Serialize)] + struct ExtractWevtEventOutputLine { + source: String, + resource: ResourceIdJson, + lang_id: u32, + provider_guid: String, + event_index: usize, + event_id: u16, + version: u8, + channel: u8, + level: u8, + opcode: u8, + task: u16, + keywords: u64, + message_identifier: u32, + template_offset: Option, + template_guid: Option, + } + + #[derive(Debug, Serialize)] + struct ExtractWevtTemplateItemOutputLine { + source: String, + resource: ResourceIdJson, + lang_id: u32, + ttbl_offset: u32, + template_offset: u32, + template_guid: String, + item_index: usize, + name: Option, + input_type: u8, + output_type: u8, + count: u16, + length: u16, + name_offset: u32, + unknown1: u32, + unknown3: u16, + unknown4: u32, + } + + pub(super) fn run_impl(matches: &ArgMatches) -> Result<()> { + use evtx::wevt_templates::{ResourceIdentifier, extract_wevt_template_resources}; + + use evtx::wevt_templates::manifest::CrimManifest; + use evtx::wevt_templates::render_template_definition_to_xml; + use std::collections::HashSet; + + let output_dir = PathBuf::from( + matches + .get_one::("output-dir") + .expect("required argument"), + ); + fs::create_dir_all(&output_dir).with_context(|| { + format!( + "failed to create output dir `{}`", + output_dir.to_string_lossy() + ) + })?; + + let overwrite = matches.get_flag("overwrite"); + let recursive = matches.get_flag("recursive"); + + let split_ttbl = matches.get_flag("split-ttbl"); + + let dump_temp_xml = matches.get_flag("dump-temp-xml"); + + let dump_events = matches.get_flag("dump-events"); + + let dump_items = matches.get_flag("dump-items"); + + let allowed_exts: HashSet = matches + .get_one::("extensions") + .expect("has default") + .split(',') + .map(|s| s.trim().trim_start_matches('.').to_ascii_lowercase()) + .filter(|s| !s.is_empty()) + .collect(); + + let mut inputs: Vec = vec![]; + + if let Some(paths) = matches.get_many::("input") { + inputs.extend(paths.map(PathBuf::from)); + } + + if let Some(patterns) = matches.get_many::("glob") { + for pat in patterns { + for entry in + glob::glob(pat).with_context(|| format!("invalid glob pattern `{pat}`"))? + { + match entry { + Ok(p) => inputs.push(p), + Err(e) => eprintln!("glob entry error: {e}"), + } + } + } + } + + if inputs.is_empty() { + bail!("No inputs provided. Use --input and/or --glob."); + } + + // Expand directories (optionally recursively) and filter by extension. + let mut files = vec![]; + let mut seen = HashSet::::new(); + + for input in inputs { + collect_input_paths(&input, recursive, &allowed_exts, &mut seen, &mut files)?; + } + + // Keep output stable-ish. + files.sort(); + + let mut error_count = 0usize; + let mut extracted_count = 0usize; + + for path in files { + let bytes = match fs::read(&path) { + Ok(b) => b, + Err(e) => { + error_count += 1; + eprintln!("failed to read `{}`: {e}", path.to_string_lossy()); + continue; + } + }; + + let resources = match extract_wevt_template_resources(&bytes) { + Ok(r) => r, + Err(e) => { + error_count += 1; + eprintln!( + "failed to extract WEVT_TEMPLATE from `{}`: {e}", + path.to_string_lossy() + ); + continue; + } + }; + + if resources.is_empty() { + continue; + } + + let source_str = path.to_string_lossy().to_string(); + let source_hash = evtx::checksum_ieee(source_str.as_bytes()); + + for res in resources { + let resource_id_str = match &res.resource { + ResourceIdentifier::Id(id) => format!("id_{id}"), + ResourceIdentifier::Name(name) => format!("name_{}", sanitize_component(name)), + }; + + let out_name = format!( + "{base}.{hash:08x}.wevt_template.{res_id}.lang_{lang}.bin", + base = path + .file_name() + .map(|s| s.to_string_lossy()) + .unwrap_or_else(|| "unknown".into()), + hash = source_hash, + res_id = resource_id_str, + lang = res.lang_id + ); + + let out_path = output_dir.join(out_name); + + if out_path.exists() && !overwrite { + continue; + } + + if let Err(e) = fs::write(&out_path, &res.data) { + error_count += 1; + eprintln!("failed to write `{}`: {e}", out_path.to_string_lossy()); + continue; + } + + extracted_count += 1; + + let resource_json = match &res.resource { + ResourceIdentifier::Id(id) => ResourceIdJson::Id(*id), + ResourceIdentifier::Name(name) => ResourceIdJson::Name(name.clone()), + }; + + let line = ExtractWevtTemplatesOutputLine { + source: source_str.clone(), + resource: resource_json, + lang_id: res.lang_id, + output_path: out_path.to_string_lossy().to_string(), + size: res.data.len(), + }; + + println!("{}", serde_json::to_string(&line)?); + + if split_ttbl || dump_temp_xml || dump_events || dump_items { + let templates_dir = output_dir.join("temp"); + let templates_xml_dir = output_dir.join("temp_xml"); + + if split_ttbl { + fs::create_dir_all(&templates_dir).with_context(|| { + format!( + "failed to create TEMP output dir `{}`", + templates_dir.to_string_lossy() + ) + })?; + } + + if dump_temp_xml { + fs::create_dir_all(&templates_xml_dir).with_context(|| { + format!( + "failed to create TEMP XML output dir `{}`", + templates_xml_dir.to_string_lossy() + ) + })?; + } + + let manifest = match CrimManifest::parse(&res.data) { + Ok(m) => m, + Err(e) => { + error_count += 1; + eprintln!( + "failed to parse CRIM/WEVT manifest in `{}`: {e}", + source_str + ); + continue; + } + }; + + let resource_json_for_records = match &res.resource { + ResourceIdentifier::Id(id) => ResourceIdJson::Id(*id), + ResourceIdentifier::Name(name) => ResourceIdJson::Name(name.clone()), + }; + + if dump_events { + for provider in &manifest.providers { + let provider_guid = format!("{}", provider.guid); + if let Some(evnt) = provider.wevt.elements.events.as_ref() { + for (event_index, ev) in evnt.events.iter().enumerate() { + let template_guid = ev + .template_offset + .and_then(|off| provider.template_by_offset(off)) + .map(|t| format!("{}", t.guid)); + + let line = ExtractWevtEventOutputLine { + source: source_str.clone(), + resource: resource_json_for_records.clone(), + lang_id: res.lang_id, + provider_guid: provider_guid.clone(), + event_index, + event_id: ev.identifier, + version: ev.version, + channel: ev.channel, + level: ev.level, + opcode: ev.opcode, + task: ev.task, + keywords: ev.keywords, + message_identifier: ev.message_identifier, + template_offset: ev.template_offset, + template_guid, + }; + + println!("{}", serde_json::to_string(&line)?); + } + } + } + } + + if dump_items { + for provider in &manifest.providers { + if let Some(ttbl) = provider.wevt.elements.templates.as_ref() { + for tpl in &ttbl.templates { + let template_guid = format!("{}", tpl.guid); + for (item_index, item) in tpl.items.iter().enumerate() { + let line = ExtractWevtTemplateItemOutputLine { + source: source_str.clone(), + resource: resource_json_for_records.clone(), + lang_id: res.lang_id, + ttbl_offset: ttbl.offset, + template_offset: tpl.offset, + template_guid: template_guid.clone(), + item_index, + name: item.name.clone(), + input_type: item.input_type, + output_type: item.output_type, + count: item.count, + length: item.length, + name_offset: item.name_offset, + unknown1: item.unknown1, + unknown3: item.unknown3, + unknown4: item.unknown4, + }; + println!("{}", serde_json::to_string(&line)?); + } + } + } + } + } + + // Keep ordering stable-ish by sorting by template offset. + let mut templates: Vec<( + u32, + &evtx::wevt_templates::manifest::TemplateDefinition<'_>, + )> = vec![]; + for provider in &manifest.providers { + if let Some(ttbl) = provider.wevt.elements.templates.as_ref() { + for tpl in &ttbl.templates { + templates.push((ttbl.offset, tpl)); + } + } + } + templates.sort_by_key(|(ttbl_off, tpl)| (tpl.offset, *ttbl_off)); + + for (idx, (ttbl_offset, tpl)) in templates.iter().enumerate() { + let temp_off = tpl.offset as usize; + let temp_end = temp_off.saturating_add(tpl.size as usize); + if temp_end > res.data.len() { + error_count += 1; + eprintln!( + "TEMP slice out of bounds for `{}` (temp_offset={}, temp_size={})", + source_str, tpl.offset, tpl.size + ); + continue; + } + + let temp_bytes = &res.data[temp_off..temp_end]; + + let guid_display = format!("{}", tpl.guid); + let guid_file = sanitize_component(&guid_display); + + if split_ttbl { + let out_name = format!( + "{base}.{hash:08x}.wevt_template.{res_id}.lang_{lang}.temp_{idx:04}.{guid}.bin", + base = path + .file_name() + .map(|s| s.to_string_lossy()) + .unwrap_or_else(|| "unknown".into()), + hash = source_hash, + res_id = resource_id_str, + lang = res.lang_id, + idx = idx, + guid = guid_file, + ); + + let temp_path = templates_dir.join(out_name); + if overwrite || !temp_path.exists() { + match fs::write(&temp_path, temp_bytes) { + Ok(()) => {} + Err(e) => { + error_count += 1; + eprintln!( + "failed to write `{}`: {e}", + temp_path.to_string_lossy() + ); + continue; + } + } + } + + let temp_line = ExtractWevtTempOutputLine { + source: source_str.clone(), + resource: match &res.resource { + ResourceIdentifier::Id(id) => ResourceIdJson::Id(*id), + ResourceIdentifier::Name(name) => { + ResourceIdJson::Name(name.clone()) + } + }, + lang_id: res.lang_id, + ttbl_offset: *ttbl_offset, + temp_offset: tpl.offset, + temp_size: tpl.size, + item_descriptor_count: tpl.item_descriptor_count, + item_name_count: tpl.item_name_count, + template_items_offset: tpl.template_items_offset, + event_type: tpl.event_type, + guid: guid_display.clone(), + output_path: temp_path.to_string_lossy().to_string(), + }; + + println!("{}", serde_json::to_string(&temp_line)?); + } + + if dump_temp_xml { + let xml_name = format!( + "{base}.{hash:08x}.wevt_template.{res_id}.lang_{lang}.temp_{idx:04}.{guid}.xml", + base = path + .file_name() + .map(|s| s.to_string_lossy()) + .unwrap_or_else(|| "unknown".into()), + hash = source_hash, + res_id = resource_id_str, + lang = res.lang_id, + idx = idx, + guid = guid_file, + ); + let xml_path = templates_xml_dir.join(xml_name); + + if overwrite || !xml_path.exists() { + match render_template_definition_to_xml( + tpl, + encoding::all::WINDOWS_1252, + ) { + Ok(xml) => { + if let Err(e) = fs::write(&xml_path, xml.as_bytes()) { + error_count += 1; + eprintln!( + "failed to write `{}`: {e}", + xml_path.to_string_lossy() + ); + continue; + } + } + Err(e) => { + error_count += 1; + eprintln!( + "failed to render TEMP XML for `{}` (temp_index={}, guid={}): {e}", + source_str, idx, guid_display + ); + continue; + } + } + } + + let xml_line = ExtractWevtTempXmlOutputLine { + source: source_str.clone(), + resource: match &res.resource { + ResourceIdentifier::Id(id) => ResourceIdJson::Id(*id), + ResourceIdentifier::Name(name) => { + ResourceIdJson::Name(name.clone()) + } + }, + lang_id: res.lang_id, + temp_index: idx, + guid: guid_display, + output_path: xml_path.to_string_lossy().to_string(), + }; + println!("{}", serde_json::to_string(&xml_line)?); + } + } + } + } + } + + if error_count > 0 { + bail!( + "extract-wevt-templates completed with {error_count} error(s) (extracted {extracted_count} resource blob(s))" + ); + } + + eprintln!("extracted {extracted_count} resource blob(s)"); + Ok(()) + } + + fn collect_input_paths( + input: &Path, + recursive: bool, + allowed_exts: &std::collections::HashSet, + seen: &mut std::collections::HashSet, + out_files: &mut Vec, + ) -> Result<()> { + use std::collections::VecDeque; + + if !input.exists() { + return Ok(()); + } + + if input.is_file() { + // For explicit files (or glob matches that are files), do not apply extension filtering. + // Users often point to unusual extensions (e.g. `services.exe` renamed to `.gif`). + let p = input.to_path_buf(); + if seen.insert(p.clone()) { + out_files.push(p); + } + return Ok(()); + } + + if input.is_dir() { + if !recursive { + // Directory input without recursion is ambiguous; ignore silently. + return Ok(()); + } + + let mut queue = VecDeque::new(); + queue.push_back(input.to_path_buf()); + + while let Some(dir) = queue.pop_front() { + let entries = match fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + + for entry in entries.flatten() { + let p = entry.path(); + if p.is_dir() { + queue.push_back(p); + } else if p.is_file() + && should_keep_file(&p, allowed_exts) + && seen.insert(p.clone()) + { + out_files.push(p); + } + } + } + } + + Ok(()) + } + + fn should_keep_file(path: &Path, allowed_exts: &std::collections::HashSet) -> bool { + let Some(ext) = path.extension().and_then(|s| s.to_str()) else { + return false; + }; + allowed_exts.contains(&ext.to_ascii_lowercase()) + } + + fn sanitize_component(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut last_underscore = false; + for ch in s.chars() { + let keep = ch.is_ascii_alphanumeric() || ch == '.' || ch == '-' || ch == '_'; + if keep { + out.push(ch); + last_underscore = false; + } else if !last_underscore { + out.push('_'); + last_underscore = true; + } + } + if out.is_empty() { + "unnamed".to_string() + } else { + out + } + } +} + +#[cfg(feature = "wevt_templates")] +use imp::run_impl; diff --git a/src/binxml/assemble.rs b/src/binxml/assemble.rs index 9a9721af..83e5fd05 100644 --- a/src/binxml/assemble.rs +++ b/src/binxml/assemble.rs @@ -5,16 +5,16 @@ use crate::model::deserialized::{ BinXMLDeserializedTokens, BinXmlTemplateRef, TemplateSubstitutionDescriptor, }; use crate::model::xml::{XmlElement, XmlElementBuilder, XmlModel, XmlPIBuilder}; +use crate::utils::ByteCursor; use crate::xml_output::BinXmlOutput; use log::{debug, trace, warn}; -use std::borrow::{BorrowMut, Cow}; +use std::borrow::Cow; use std::mem; use crate::EvtxChunk; use crate::binxml::name::{BinXmlName, BinXmlNameRef}; -use crate::binxml::tokens::read_template_definition; -use std::io::{Cursor, Seek, SeekFrom}; +use crate::binxml::tokens::read_template_definition_cursor; pub fn parse_tokens<'a, T: BinXmlOutput>( tokens: Vec>, @@ -196,15 +196,11 @@ fn expand_string_ref<'a>( match chunk.string_cache.get_cached_string(string_ref.offset) { Some(s) => Ok(Cow::Borrowed(s)), None => { - let mut cursor = Cursor::new(chunk.data); - let cursor_ref = cursor.borrow_mut(); - try_seek!( - cursor_ref, - string_ref.offset + BINXML_NAME_LINK_SIZE, - "Cache missed string" + let name_off = string_ref.offset.checked_add(BINXML_NAME_LINK_SIZE).ok_or( + EvtxError::FailedToCreateRecordModel("string table offset overflow"), )?; - - let string = BinXmlName::from_stream(cursor_ref)?; + let mut cursor = ByteCursor::with_pos(chunk.data, name_off as usize)?; + let string = BinXmlName::from_cursor(&mut cursor)?; Ok(Cow::Owned(string)) } } @@ -267,11 +263,12 @@ fn expand_template<'a>( template.template_def_offset ); - let mut cursor = Cursor::new(chunk.data); - - let _ = cursor.seek(SeekFrom::Start(u64::from(template.template_def_offset))); - let template_def = - read_template_definition(&mut cursor, Some(chunk), chunk.settings.get_ansi_codec())?; + let mut cursor = ByteCursor::with_pos(chunk.data, template.template_def_offset as usize)?; + let template_def = read_template_definition_cursor( + &mut cursor, + Some(chunk), + chunk.settings.get_ansi_codec(), + )?; for token in template_def.tokens { if let BinXMLDeserializedTokens::Substitution(substitution_descriptor) = token { @@ -442,9 +439,9 @@ fn stream_expand_token<'a, T: BinXmlOutput>( } } } else { - let mut cursor = Cursor::new(chunk.data); - let _ = cursor.seek(SeekFrom::Start(u64::from(template.template_def_offset))); - let template_def = read_template_definition( + let mut cursor = + ByteCursor::with_pos(chunk.data, template.template_def_offset as usize)?; + let template_def = read_template_definition_cursor( &mut cursor, Some(chunk), chunk.settings.get_ansi_codec(), diff --git a/src/binxml/deserializer.rs b/src/binxml/deserializer.rs index ada94015..c8e8ecfc 100644 --- a/src/binxml/deserializer.rs +++ b/src/binxml/deserializer.rs @@ -1,46 +1,47 @@ use crate::err::{DeserializationError, DeserializationResult as Result}; - -use byteorder::ReadBytesExt; +use crate::utils::ByteCursor; use log::trace; -use std::io::{Seek, SeekFrom}; +use crate::binxml::name::BinXmlNameEncoding; use crate::binxml::tokens::{ - read_open_start_element, read_processing_instruction_data, read_processing_instruction_target, + read_attribute_cursor, read_entity_ref_cursor, read_fragment_header_cursor, + read_open_start_element_cursor, read_processing_instruction_data_cursor, + read_processing_instruction_target_cursor, read_substitution_descriptor_cursor, + read_template_cursor, }; use crate::binxml::value_variant::BinXmlValue; -use crate::{ - binxml::tokens::{ - read_attribute, read_entity_ref, read_fragment_header, read_substitution_descriptor, - read_template, - }, - model::{deserialized::*, raw::*}, -}; +use crate::model::{deserialized::*, raw::*}; use crate::evtx_chunk::EvtxChunk; use encoding::EncodingRef; use std::io::Cursor; -use std::mem; pub struct IterTokens<'a> { - cursor: Cursor<&'a [u8]>, + cursor: ByteCursor<'a>, chunk: Option<&'a EvtxChunk<'a>>, data_size: Option, data_read_so_far: u32, eof: bool, - is_inside_substitution: bool, + /// Whether element start headers include the dependency identifier (u16). + /// + /// - Template definitions: true + /// - Direct record elements and nested BinXML substitution values (0x21): false + has_dep_id: bool, ansi_codec: EncodingRef, + name_encoding: BinXmlNameEncoding, } pub struct BinXmlDeserializer<'a> { data: &'a [u8], offset: u64, chunk: Option<&'a EvtxChunk<'a>>, - // if called from substitution token with value type: Binary XML (0x21) - is_inside_substitution: bool, + /// Whether element start headers include the dependency identifier (u16). + has_dep_id: bool, ansi_codec: EncodingRef, + name_encoding: BinXmlNameEncoding, } impl<'a> BinXmlDeserializer<'a> { @@ -48,15 +49,34 @@ impl<'a> BinXmlDeserializer<'a> { data: &'a [u8], start_offset: u64, chunk: Option<&'a EvtxChunk<'a>>, - is_inside_substitution: bool, + has_dep_id: bool, + ansi_codec: EncodingRef, + ) -> Self { + BinXmlDeserializer { + data, + offset: start_offset, + chunk, + has_dep_id, + ansi_codec, + name_encoding: BinXmlNameEncoding::Offset, + } + } + + pub fn init_with_name_encoding( + data: &'a [u8], + start_offset: u64, + chunk: Option<&'a EvtxChunk<'a>>, + has_dep_id: bool, ansi_codec: EncodingRef, + name_encoding: BinXmlNameEncoding, ) -> Self { BinXmlDeserializer { data, offset: start_offset, chunk, - is_inside_substitution, + has_dep_id, ansi_codec, + name_encoding, } } @@ -65,41 +85,45 @@ impl<'a> BinXmlDeserializer<'a> { cursor: &mut Cursor<&'a [u8]>, chunk: Option<&'a EvtxChunk<'a>>, data_size: Option, - is_inside_substitution: bool, + has_dep_id: bool, ansi_codec: EncodingRef, ) -> Result>> { let offset = cursor.position(); - let de = BinXmlDeserializer::init( - cursor.get_ref(), - offset, - chunk, - is_inside_substitution, - ansi_codec, - ); + let de = BinXmlDeserializer::init(cursor.get_ref(), offset, chunk, has_dep_id, ansi_codec); let mut tokens = vec![]; let mut iterator = de.iter_tokens(data_size)?; loop { let token = iterator.next(); - match token { Some(t) => { - tokens.push(t?); - } _ => { - break; - }} + match token { + Some(t) => { + tokens.push(t?); + } + _ => { + break; + } + } } - let seek_ahead = iterator.cursor.position() - offset; - cursor.seek(SeekFrom::Current(seek_ahead as i64))?; + // `IterTokens` holds an absolute position in the original slice. + cursor.set_position(iterator.position()); Ok(tokens) } /// Reads `data_size` bytes of binary xml, or until EOF marker. pub fn iter_tokens(self, data_size: Option) -> Result> { - let mut cursor = Cursor::new(self.data); - cursor.seek(SeekFrom::Start(self.offset))?; + let cursor = ByteCursor::with_pos( + self.data, + usize::try_from(self.offset).map_err(|_| DeserializationError::Truncated { + what: "BinXmlDeserializer.offset", + offset: self.offset, + need: 0, + have: 0, + })?, + )?; Ok(IterTokens { cursor, @@ -107,17 +131,22 @@ impl<'a> BinXmlDeserializer<'a> { data_size, data_read_so_far: 0, eof: false, - is_inside_substitution: self.is_inside_substitution, + has_dep_id: self.has_dep_id, ansi_codec: self.ansi_codec, + name_encoding: self.name_encoding, }) } } impl<'a> IterTokens<'a> { + pub fn position(&self) -> u64 { + self.cursor.position() + } + /// Reads the next token from the stream, will return error if failed to read from the stream for some reason, /// or if reading random bytes (usually because of a bug in the code). - fn read_next_token(&self, cursor: &mut Cursor<&'a [u8]>) -> Result { - let token = try_read!(cursor, u8)?; + fn read_next_token(&self, cursor: &mut ByteCursor<'a>) -> Result { + let token = cursor.u8()?; BinXMLRawToken::from_u8(token).ok_or(DeserializationError::InvalidToken { value: token, @@ -127,7 +156,7 @@ impl<'a> IterTokens<'a> { fn visit_token( &self, - cursor: &mut Cursor<&'a [u8]>, + cursor: &mut ByteCursor<'a>, raw_token: BinXMLRawToken, ) -> Result> { match raw_token { @@ -135,11 +164,11 @@ impl<'a> IterTokens<'a> { BinXMLRawToken::OpenStartElement(token_information) => { // Debug print inside Ok(BinXMLDeserializedTokens::OpenStartElement( - read_open_start_element( + read_open_start_element_cursor( cursor, - self.chunk, token_information.has_attributes, - self.is_inside_substitution, + self.has_dep_id, + self.name_encoding, )?, )) } @@ -147,10 +176,13 @@ impl<'a> IterTokens<'a> { BinXMLRawToken::CloseEmptyElement => Ok(BinXMLDeserializedTokens::CloseEmptyElement), BinXMLRawToken::CloseElement => Ok(BinXMLDeserializedTokens::CloseElement), BinXMLRawToken::Value => Ok(BinXMLDeserializedTokens::Value( - BinXmlValue::from_binxml_stream(cursor, self.chunk, None, self.ansi_codec)?, + BinXmlValue::from_binxml_cursor(cursor, self.chunk, None, self.ansi_codec)?, )), BinXMLRawToken::Attribute(_token_information) => { - Ok(BinXMLDeserializedTokens::Attribute(read_attribute(cursor)?)) + Ok(BinXMLDeserializedTokens::Attribute(read_attribute_cursor( + cursor, + self.name_encoding, + )?)) } BinXMLRawToken::CDataSection => Err(DeserializationError::UnimplementedToken { name: "CDataSection", @@ -161,25 +193,25 @@ impl<'a> IterTokens<'a> { offset: cursor.position(), }), BinXMLRawToken::EntityReference => Ok(BinXMLDeserializedTokens::EntityRef( - read_entity_ref(cursor)?, + read_entity_ref_cursor(cursor, self.name_encoding)?, )), BinXMLRawToken::ProcessingInstructionTarget => Ok(BinXMLDeserializedTokens::PITarget( - read_processing_instruction_target(cursor)?, + read_processing_instruction_target_cursor(cursor, self.name_encoding)?, )), BinXMLRawToken::ProcessingInstructionData => Ok(BinXMLDeserializedTokens::PIData( - read_processing_instruction_data(cursor)?, + read_processing_instruction_data_cursor(cursor)?, )), BinXMLRawToken::TemplateInstance => Ok(BinXMLDeserializedTokens::TemplateInstance( - read_template(cursor, self.chunk, self.ansi_codec)?, + read_template_cursor(cursor, self.chunk, self.ansi_codec)?, )), BinXMLRawToken::NormalSubstitution => Ok(BinXMLDeserializedTokens::Substitution( - read_substitution_descriptor(cursor, false)?, + read_substitution_descriptor_cursor(cursor, false)?, )), BinXMLRawToken::ConditionalSubstitution => Ok(BinXMLDeserializedTokens::Substitution( - read_substitution_descriptor(cursor, true)?, + read_substitution_descriptor_cursor(cursor, true)?, )), BinXMLRawToken::StartOfStream => Ok(BinXMLDeserializedTokens::FragmentHeader( - read_fragment_header(cursor)?, + read_fragment_header_cursor(cursor)?, )), } } @@ -187,7 +219,7 @@ impl<'a> IterTokens<'a> { impl<'a> IterTokens<'a> { fn inner_next(&mut self) -> Option>> { - let mut cursor = self.cursor.clone(); + let mut cursor = self.cursor; let offset_from_chunk_start = cursor.position(); trace!( @@ -233,7 +265,7 @@ impl<'a> IterTokens<'a> { let total_read = cursor.position() - offset_from_chunk_start; self.data_read_so_far += total_read as u32; - mem::swap(&mut self.cursor, &mut cursor); + self.cursor = cursor; yield_value } } @@ -249,8 +281,10 @@ impl<'a> Iterator for IterTokens<'a> { #[cfg(test)] mod tests { + use super::BinXmlDeserializer; + use crate::binxml::name::{BinXmlNameEncoding, read_wevt_inline_name_at}; use crate::evtx_chunk::EvtxChunkData; - use crate::{ensure_env_logger_initialized, ParserSettings}; + use crate::{ParserSettings, ensure_env_logger_initialized}; use std::sync::Arc; #[test] @@ -281,13 +315,15 @@ mod tests { let records = evtx_chunk.iter(); for record in records.take(100) { - assert!(!record - .unwrap() - .into_xml() - .unwrap() - .data - .chars() - .any(|c| c == '\0')) + assert!( + !record + .unwrap() + .into_xml() + .unwrap() + .data + .chars() + .any(|c| c == '\0') + ) } } @@ -314,4 +350,104 @@ mod tests { } } } + + #[test] + fn test_reads_wevt_inline_names() { + // Minimal fragment: + let mut buf = vec![]; + // Fragment header (token 0x0f) + version 1.1 + flags 0 + buf.extend_from_slice(&[0x0f, 0x01, 0x01, 0x00]); + // OpenStartElement (0x01) + buf.push(0x01); + // dependency identifier + buf.extend_from_slice(&0xFFFFu16.to_le_bytes()); + // data size + buf.extend_from_slice(&0x10u32.to_le_bytes()); + // inline name: hash + char_count + utf16 + NUL + let name = "EventData"; + let name_hash = + crate::binxml::name::compute_wevt_inline_name_hash_utf16(name.encode_utf16()); + buf.extend_from_slice(&name_hash.to_le_bytes()); + buf.extend_from_slice(&(name.encode_utf16().count() as u16).to_le_bytes()); + for c in name.encode_utf16() { + buf.extend_from_slice(&c.to_le_bytes()); + } + buf.extend_from_slice(&0u16.to_le_bytes()); + // CloseEmptyElement + EndOfStream + buf.extend_from_slice(&[0x03, 0x00]); + + let de = BinXmlDeserializer::init_with_name_encoding( + &buf, + 0, + None, + true, + encoding::all::WINDOWS_1252, + BinXmlNameEncoding::WevtInline, + ); + + let mut iterator = de.iter_tokens(None).expect("iter_tokens"); + let mut tokens = vec![]; + while let Some(t) = iterator.next() { + tokens.push(t.expect("token")); + } + + assert!( + matches!( + tokens.first(), + Some(crate::model::deserialized::BinXMLDeserializedTokens::FragmentHeader(_)) + ), + "expected FragmentHeader first, got {tokens:?}" + ); + + let open = tokens.iter().find_map(|t| match t { + crate::model::deserialized::BinXMLDeserializedTokens::OpenStartElement(e) => Some(e), + _ => None, + }); + let open = open.expect("expected OpenStartElement token"); + + let parsed_name = + read_wevt_inline_name_at(&buf, open.name.offset).expect("read_wevt_inline_name_at"); + assert_eq!(parsed_name.as_str(), name); + } + + #[test] + fn test_wevt_inline_name_hash_mismatch_is_error() { + // Same as `test_reads_wevt_inline_names`, but with an incorrect NameHash. + let mut buf = vec![]; + buf.extend_from_slice(&[0x0f, 0x01, 0x01, 0x00]); // StartOfStream + fragment header + buf.push(0x01); // OpenStartElement + buf.extend_from_slice(&0xFFFFu16.to_le_bytes()); // dependency identifier + buf.extend_from_slice(&0x10u32.to_le_bytes()); // data size + + let name = "EventData"; + let wrong_hash = 0x1234u16; + buf.extend_from_slice(&wrong_hash.to_le_bytes()); + buf.extend_from_slice(&(name.encode_utf16().count() as u16).to_le_bytes()); + for c in name.encode_utf16() { + buf.extend_from_slice(&c.to_le_bytes()); + } + buf.extend_from_slice(&0u16.to_le_bytes()); + + buf.extend_from_slice(&[0x03, 0x00]); // CloseEmptyElement + EndOfStream + + let de = BinXmlDeserializer::init_with_name_encoding( + &buf, + 0, + None, + true, + encoding::all::WINDOWS_1252, + BinXmlNameEncoding::WevtInline, + ); + + let mut iterator = de.iter_tokens(None).expect("iter_tokens"); + while let Some(t) = iterator.next() { + match t { + Ok(_) => continue, + Err(crate::err::DeserializationError::WevtInlineNameHashMismatch { .. }) => return, + Err(e) => panic!("unexpected error: {e:?}"), + } + } + + panic!("expected WevtInlineNameHashMismatch error"); + } } diff --git a/src/binxml/name.rs b/src/binxml/name.rs index 3d8b00de..a3860684 100644 --- a/src/binxml/name.rs +++ b/src/binxml/name.rs @@ -1,23 +1,46 @@ +use crate::err::DeserializationError; use crate::err::DeserializationResult as Result; use crate::ChunkOffset; -pub use byteorder::{LittleEndian, ReadBytesExt}; +use crate::utils::ByteCursor; -use crate::utils::read_len_prefixed_utf16_string; - -use std::{ - fmt::Formatter, - io::{Cursor, Seek, SeekFrom}, -}; +use std::{fmt::Formatter, io::Cursor}; use quick_xml::events::{BytesEnd, BytesStart}; use std::fmt; +const WEVT_INLINE_NAME_HASH_MULTIPLIER: u32 = 65599; + +/// MS-EVEN6 NameHash (low 16 bits of: hash=0; for each UTF-16 code unit: hash = hash*65599 + code_unit). +#[cfg(test)] +pub(crate) fn compute_wevt_inline_name_hash_utf16( + code_units: impl IntoIterator, +) -> u16 { + let mut hash: u32 = 0; + for cu in code_units { + hash = hash + .wrapping_mul(WEVT_INLINE_NAME_HASH_MULTIPLIER) + .wrapping_add(u32::from(cu)); + } + (hash & 0xffff) as u16 +} + #[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Hash)] pub struct BinXmlName { str: String, } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum BinXmlNameEncoding { + /// Standard EVTX encoding where names are referenced by offsets into the chunk string table. + Offset, + /// WEVT_TEMPLATE / CRIM 5.x encoding where names are stored inline as: + /// `u16 name_hash` + `u16 char_count` + `UTF-16LE chars` + `u16 NUL`. + /// + /// Primary reference: MS-EVEN6 (`Name = NameHash NameNumChars NullTerminatedUnicodeString`). + WevtInline, +} + #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)] pub struct BinXmlNameRef { pub offset: ChunkOffset, @@ -36,9 +59,9 @@ pub(crate) struct BinXmlNameLink { } impl BinXmlNameLink { - pub fn from_stream(stream: &mut Cursor<&[u8]>) -> Result { - let next_string = try_read!(stream, u32)?; - let name_hash = try_read!(stream, u16, "name_hash")?; + pub(crate) fn from_cursor(cursor: &mut ByteCursor<'_>) -> Result { + let next_string = cursor.u32()?; + let name_hash = cursor.u16_named("name_hash")?; Ok(BinXmlNameLink { next_string: if next_string > 0 { @@ -56,30 +79,113 @@ impl BinXmlNameLink { } impl BinXmlNameRef { - pub fn from_stream(cursor: &mut Cursor<&[u8]>) -> Result { - let name_offset = try_read!(cursor, u32, "name_offset")?; + pub(crate) fn from_cursor(cursor: &mut ByteCursor<'_>) -> Result { + let name_offset = cursor.u32_named("name_offset")?; let position_before_string = cursor.position(); let need_to_seek = position_before_string == u64::from(name_offset); if need_to_seek { - let _ = BinXmlNameLink::from_stream(cursor)?; - let len = cursor.read_u16::()?; + let _ = BinXmlNameLink::from_cursor(cursor)?; + let len = cursor.u16_named("string_table_name_len")?; let nul_terminator_len = 4; let data_size = BinXmlNameLink::data_size() + u32::from(len * 2) + nul_terminator_len; - try_seek!( - cursor, - position_before_string + u64::from(data_size), - "Skip string" - )?; + cursor.set_pos_u64(position_before_string + u64::from(data_size), "Skip string")?; + } + + Ok(BinXmlNameRef { + offset: name_offset, + }) + } + + pub fn from_stream(cursor: &mut Cursor<&[u8]>) -> Result { + let start = cursor.position() as usize; + let buf = *cursor.get_ref(); + let mut c = ByteCursor::with_pos(buf, start)?; + let v = Self::from_cursor(&mut c)?; + cursor.set_position(c.position()); + Ok(v) + } + + pub fn from_stream_with_encoding( + cursor: &mut Cursor<&[u8]>, + encoding: BinXmlNameEncoding, + ) -> Result { + match encoding { + BinXmlNameEncoding::Offset => Self::from_stream(cursor), + BinXmlNameEncoding::WevtInline => Self::from_stream_wevt_inline(cursor), + } + } + + pub(crate) fn from_cursor_with_encoding( + cursor: &mut ByteCursor<'_>, + encoding: BinXmlNameEncoding, + ) -> Result { + match encoding { + BinXmlNameEncoding::Offset => Self::from_cursor(cursor), + BinXmlNameEncoding::WevtInline => Self::from_cursor_wevt_inline(cursor), + } + } + + fn from_cursor_wevt_inline(cursor: &mut ByteCursor<'_>) -> Result { + let name_offset = cursor.position() as ChunkOffset; + let stored_hash = cursor.u16_named("wevt_inline_name_hash")?; + // character count + let char_count = cursor.u16_named("wevt_inline_name_character_count")?; + + let mut hash: u32 = 0; + for _ in 0..char_count { + let code_unit = cursor.u16_named("wevt_inline_name_code_unit")?; + hash = hash + .wrapping_mul(WEVT_INLINE_NAME_HASH_MULTIPLIER) + .wrapping_add(u32::from(code_unit)); + } + + let nul = cursor.u16_named("wevt_inline_name_nul")?; + if nul != 0 { + return Err(DeserializationError::WevtInlineNameMissingNulTerminator { + found: nul, + offset: u64::from(name_offset), + }); + } + + let expected_hash = (hash & 0xffff) as u16; + if stored_hash != expected_hash { + return Err(DeserializationError::WevtInlineNameHashMismatch { + expected: expected_hash, + found: stored_hash, + offset: u64::from(name_offset), + }); } Ok(BinXmlNameRef { offset: name_offset, }) } + + fn from_stream_wevt_inline(cursor: &mut Cursor<&[u8]>) -> Result { + let start = cursor.position() as usize; + let buf = *cursor.get_ref(); + let mut c = ByteCursor::with_pos(buf, start)?; + let v = Self::from_cursor_wevt_inline(&mut c)?; + cursor.set_position(c.position()); + Ok(v) + } +} + +/// Resolve a WEVT inline name at the given offset. +/// +/// The offset should point to the start of the inline name structure, i.e. the `name_hash` field. +#[cfg(any(test, feature = "wevt_templates"))] +pub(crate) fn read_wevt_inline_name_at(data: &[u8], offset: ChunkOffset) -> Result { + let mut cursor = ByteCursor::with_pos(data, offset as usize)?; + let _ = cursor.u16_named("wevt_inline_name_hash")?; + let name = cursor + .len_prefixed_utf16_string(true, "wevt_inline_name")? + .unwrap_or_default(); + Ok(BinXmlName { str: name }) } impl BinXmlName { @@ -95,9 +201,18 @@ impl BinXmlName { /// Reads a tuple of (String, Hash, Offset) from a stream. pub fn from_stream(cursor: &mut Cursor<&[u8]>) -> Result { - let name = - try_read!(cursor, len_prefixed_utf_16_str_nul_terminated, "name")?.unwrap_or_default(); + let start = cursor.position() as usize; + let buf = *cursor.get_ref(); + let mut c = ByteCursor::with_pos(buf, start)?; + let v = Self::from_cursor(&mut c)?; + cursor.set_position(c.position()); + Ok(v) + } + pub(crate) fn from_cursor(cursor: &mut ByteCursor<'_>) -> Result { + let name = cursor + .len_prefixed_utf16_string(true, "name")? + .unwrap_or_default(); Ok(BinXmlName { str: name }) } diff --git a/src/binxml/tokens.rs b/src/binxml/tokens.rs index 99e7ef49..916474e0 100644 --- a/src/binxml/tokens.rs +++ b/src/binxml/tokens.rs @@ -1,52 +1,60 @@ -use crate::err::{DeserializationError, DeserializationResult as Result, WrappedIoError}; +use crate::err::{DeserializationError, DeserializationResult as Result}; -pub use byteorder::ReadBytesExt; use winstructs::guid::Guid; use crate::model::deserialized::*; +use crate::utils::ByteCursor; use std::io::Cursor; use crate::binxml::deserializer::BinXmlDeserializer; -use crate::binxml::name::BinXmlNameRef; +use crate::binxml::name::{BinXmlNameEncoding, BinXmlNameRef}; use crate::binxml::value_variant::{BinXmlValue, BinXmlValueType}; -use crate::utils::read_len_prefixed_utf16_string; use log::{error, trace, warn}; -use std::io::Seek; -use std::io::SeekFrom; - use crate::evtx_chunk::EvtxChunk; use encoding::EncodingRef; -pub fn read_template<'a>( - cursor: &mut Cursor<&'a [u8]>, +fn with_cursor<'a, T>( + cursor: &mut ByteCursor<'a>, + f: impl FnOnce(&mut Cursor<&'a [u8]>) -> Result, +) -> Result { + let mut c = Cursor::new(cursor.buf()); + c.set_position(cursor.position()); + let out = f(&mut c)?; + cursor.set_pos_u64(c.position(), "advance after cursor-backed parse")?; + Ok(out) +} + +pub(crate) fn read_template_cursor<'a>( + cursor: &mut ByteCursor<'a>, chunk: Option<&'a EvtxChunk<'a>>, ansi_codec: EncodingRef, ) -> Result> { trace!("TemplateInstance at {}", cursor.position()); - let _ = try_read!(cursor, u8)?; - let _template_id = try_read!(cursor, u32)?; - let template_definition_data_offset = try_read!(cursor, u32)?; + let _ = cursor.u8()?; + let template_id = cursor.u32()?; + let template_definition_data_offset = cursor.u32()?; + let mut template_guid: Option = None; // Need to skip over the template data. if (cursor.position() as u32) == template_definition_data_offset { - let template_header = read_template_definition_header(cursor)?; - try_seek!( - cursor, + let template_header = read_template_definition_header_cursor(cursor)?; + template_guid = Some(template_header.guid.clone()); + cursor.set_pos_u64( cursor.position() + u64::from(template_header.data_size), - "Skip cached template" + "Skip cached template", )?; } - let number_of_substitutions = try_read!(cursor, u32)?; + let number_of_substitutions = cursor.u32()?; let mut value_descriptors = Vec::with_capacity(number_of_substitutions as usize); for _ in 0..number_of_substitutions { - let size = try_read!(cursor, u16)?; - let value_type_token = try_read!(cursor, u8)?; + let size = cursor.u16()?; + let value_type_token = cursor.u8()?; let value_type = BinXmlValueType::from_u8(value_type_token).ok_or( DeserializationError::InvalidValueVariant { @@ -56,7 +64,7 @@ pub fn read_template<'a>( )?; // Empty - let _ = try_read!(cursor, u8)?; + let _ = cursor.u8()?; value_descriptors.push(TemplateValueDescriptor { size, value_type }) } @@ -72,7 +80,8 @@ pub fn read_template<'a>( offset = position_before_reading_value, substitution = descriptor.value_type, ); - let value = BinXmlValue::deserialize_value_type( + + let value = BinXmlValue::deserialize_value_type_cursor( &descriptor.value_type, cursor, chunk, @@ -84,10 +93,9 @@ pub fn read_template<'a>( // NullType can mean deleted substitution (and data need to be skipped) if value == BinXmlValue::NullType { trace!("\t Skipping `NullType` descriptor"); - try_seek!( - cursor, + cursor.set_pos_u64( cursor.position() + u64::from(descriptor.size), - "NullType Descriptor" + "NullType Descriptor", )?; } @@ -98,14 +106,14 @@ pub fn read_template<'a>( let diff = expected_position as i128 - current_position as i128; // This sometimes occurs with dirty samples, but it's usually still possible to recover the rest of the record. // Sometimes however the log will contain a lot of zero fields. - warn!("Read incorrect amount of data, cursor position is at {}, but should have ended up at {}, last descriptor was {:?}.", - current_position, - expected_position, - &descriptor); + warn!( + "Read incorrect amount of data, cursor position is at {}, but should have ended up at {}, last descriptor was {:?}.", + current_position, expected_position, &descriptor + ); match u64::try_from(diff) { Ok(u64_diff) => { - try_seek!(cursor, current_position + u64_diff, "Broken record")?; + cursor.set_pos_u64(current_position + u64_diff, "Broken record")?; } Err(_) => error!("Broken record"), } @@ -114,20 +122,28 @@ pub fn read_template<'a>( } Ok(BinXmlTemplateRef { + template_id, template_def_offset: template_definition_data_offset, + template_guid, substitution_array, }) } -pub fn read_template_definition_header( - cursor: &mut Cursor<&[u8]>, +fn read_template_definition_header_cursor( + cursor: &mut ByteCursor<'_>, ) -> Result { // If any of these fail we cannot reliably report the template information in error. - let next_template_offset = try_read!(cursor, u32, "next_template_offset")?; - let template_guid = try_read!(cursor, guid, "template_guid")?; + let next_template_offset = cursor.u32_named("next_template_offset")?; + let guid_bytes = cursor.take_bytes(16, "template_guid")?; + let template_guid = Guid::from_buffer(guid_bytes).map_err(|_| { + DeserializationError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "invalid GUID", + )) + })?; // Data size includes the fragment header, element and end of file token; // except for the first 33 bytes of the template definition (above) - let data_size = try_read!(cursor, u32, "template_data_size")?; + let data_size = cursor.u32_named("template_data_size")?; Ok(BinXmlTemplateDefinitionHeader { next_template_offset, @@ -136,12 +152,12 @@ pub fn read_template_definition_header( }) } -pub fn read_template_definition<'a>( - cursor: &mut Cursor<&'a [u8]>, +pub(crate) fn read_template_definition_cursor<'a>( + cursor: &mut ByteCursor<'a>, chunk: Option<&'a EvtxChunk<'a>>, ansi_codec: EncodingRef, ) -> Result> { - let header = read_template_definition_header(cursor)?; + let header = read_template_definition_header_cursor(cursor)?; trace!( "Offset `0x{:08x}` - TemplateDefinition {}", @@ -149,45 +165,115 @@ pub fn read_template_definition<'a>( header ); - let template = match BinXmlDeserializer::read_binxml_fragment( - cursor, - chunk, - Some(header.data_size), - false, - ansi_codec, - ) { + let template = match with_cursor(cursor, |c| { + BinXmlDeserializer::read_binxml_fragment(c, chunk, Some(header.data_size), true, ansi_codec) + }) { Ok(tokens) => BinXMLTemplateDefinition { header, tokens }, Err(e) => { return Err(DeserializationError::FailedToDeserializeTemplate { template_id: header.guid, source: Box::new(e), - }) + }); } }; Ok(template) } -pub fn read_entity_ref(cursor: &mut Cursor<&[u8]>) -> Result { +/// Strictly read a `TemplateDefinitionHeader` at a known offset in an EVTX chunk buffer. +/// +/// This does **not** scan for signatures or guess offsets. It only succeeds when the bytes at the +/// provided `offset` look like a valid template definition header followed by a BinXML fragment +/// header (`StartOfStream` + version tuple). This is used by higher-level "offline WEVT cache" +/// logic to match a record's `TemplateInstance` to a template GUID without fully deserializing the +/// template. +pub(crate) fn try_read_template_definition_header_at( + chunk_data: &[u8], + offset: u32, +) -> Result { + let off = offset as usize; + let mut cursor = ByteCursor::with_pos(chunk_data, off)?; + + // Read the header using the canonical parser. + let header = read_template_definition_header_cursor(&mut cursor)?; + + // Validate next_template_offset is either: + // - 0 (end of list) + // - equal to itself (observed termination sentinel) + // - a forward in-chunk offset + if header.next_template_offset != 0 && header.next_template_offset != offset { + if header.next_template_offset <= offset { + return Err(DeserializationError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "template next_template_offset is not forward", + ))); + } + if (header.next_template_offset as usize) >= chunk_data.len() { + return Err(DeserializationError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "template next_template_offset out of bounds", + ))); + } + } + + // We should now be positioned immediately after the template header. + let data_size_usize = header.data_size as usize; + if data_size_usize < 4 { + return Err(DeserializationError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "template data_size too small", + ))); + } + + // Ensure the full template fragment range is in-bounds (strict; we do not accept a header that + // points past the chunk end). + let data_start = cursor.pos(); + let data_end = data_start.saturating_add(data_size_usize); + if data_end > chunk_data.len() { + return Err(DeserializationError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "template data_size out of bounds", + ))); + } + + // Verify BinXML fragment header: StartOfStream (0x0f) + major/minor/flags. + let frag = cursor.take_bytes(4, "template fragment header")?; + if frag[0] != 0x0f || frag[1] != 0x01 || frag[2] != 0x01 { + return Err(DeserializationError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "template does not start with BinXML fragment header (StartOfStream 1.1)", + ))); + } + + Ok(header) +} + +pub(crate) fn read_entity_ref_cursor( + cursor: &mut ByteCursor<'_>, + name_encoding: BinXmlNameEncoding, +) -> Result { trace!("Offset `0x{:08x}` - EntityReference", cursor.position()); - let name = BinXmlNameRef::from_stream(cursor)?; + let name = BinXmlNameRef::from_cursor_with_encoding(cursor, name_encoding)?; trace!("\t name: {:?}", name); - Ok(BinXmlEntityReference { name }) } -pub fn read_attribute(cursor: &mut Cursor<&[u8]>) -> Result { +pub(crate) fn read_attribute_cursor( + cursor: &mut ByteCursor<'_>, + name_encoding: BinXmlNameEncoding, +) -> Result { trace!("Offset `0x{:08x}` - Attribute", cursor.position()); - let name = BinXmlNameRef::from_stream(cursor)?; - + let name = BinXmlNameRef::from_cursor_with_encoding(cursor, name_encoding)?; Ok(BinXMLAttribute { name }) } -pub fn read_fragment_header(cursor: &mut Cursor<&[u8]>) -> Result { +pub(crate) fn read_fragment_header_cursor( + cursor: &mut ByteCursor<'_>, +) -> Result { trace!("Offset `0x{:08x}` - FragmentHeader", cursor.position()); - let major_version = try_read!(cursor, u8, "fragment_header_major_version")?; - let minor_version = try_read!(cursor, u8, "fragment_header_minor_version")?; - let flags = try_read!(cursor, u8, "fragment_header_flags")?; + let major_version = cursor.u8_named("fragment_header_major_version")?; + let minor_version = cursor.u8_named("fragment_header_minor_version")?; + let flags = cursor.u8_named("fragment_header_flags")?; Ok(BinXMLFragmentHeader { major_version, minor_version, @@ -195,32 +281,37 @@ pub fn read_fragment_header(cursor: &mut Cursor<&[u8]>) -> Result, +pub(crate) fn read_processing_instruction_target_cursor( + cursor: &mut ByteCursor<'_>, + name_encoding: BinXmlNameEncoding, ) -> Result { trace!( "Offset `0x{:08x}` - ProcessingInstructionTarget", cursor.position(), ); - let name = BinXmlNameRef::from_stream(cursor)?; + let name = BinXmlNameRef::from_cursor_with_encoding(cursor, name_encoding)?; trace!("\tPITarget Name - {:?}", name); Ok(BinXMLProcessingInstructionTarget { name }) } -pub fn read_processing_instruction_data(cursor: &mut Cursor<&[u8]>) -> Result { +pub(crate) fn read_processing_instruction_data_cursor( + cursor: &mut ByteCursor<'_>, +) -> Result { trace!( "Offset `0x{:08x}` - ProcessingInstructionTarget", cursor.position(), ); - let data = try_read!(cursor, len_prefixed_utf_16_str, "pi_data")?.unwrap_or_default(); + let data = cursor + .len_prefixed_utf16_string(false, "pi_data")? + .unwrap_or_default(); trace!("PIData - {}", data,); Ok(data) } -pub fn read_substitution_descriptor( - cursor: &mut Cursor<&[u8]>, +pub(crate) fn read_substitution_descriptor_cursor( + cursor: &mut ByteCursor<'_>, optional: bool, ) -> Result { trace!( @@ -228,8 +319,8 @@ pub fn read_substitution_descriptor( cursor.position(), optional ); - let substitution_index = try_read!(cursor, u16)?; - let value_type_token = try_read!(cursor, u8)?; + let substitution_index = cursor.u16()?; + let value_type_token = cursor.u8()?; let value_type = BinXmlValueType::from_u8(value_type_token).ok_or( DeserializationError::InvalidValueVariant { @@ -247,60 +338,39 @@ pub fn read_substitution_descriptor( }) } -pub fn read_open_start_element( - cursor: &mut Cursor<&[u8]>, - chunk: Option<&EvtxChunk>, +pub(crate) fn read_open_start_element_cursor( + cursor: &mut ByteCursor<'_>, has_attributes: bool, - is_substitution: bool, + has_dependency_identifier: bool, + name_encoding: BinXmlNameEncoding, ) -> Result { trace!( - "Offset `0x{:08x}` - OpenStartElement", + "Offset `0x{:08x}` - OpenStartElement", cursor.position(), has_attributes, - is_substitution + has_dependency_identifier ); - // According to https://github.com/libyal/libevtx/blob/master/documentation/Windows%20XML%20Event%20Log%20(EVTX).asciidoc - // The dependency identifier is not present when the element start is used in a substitution token. - if !is_substitution { - let _dependency_identifier = - try_read!(cursor, u16, "open_start_element_dependency_identifier")?; + // Element start headers come in (at least) two variants: + // - Template definitions: include a dependency identifier (u16) + // - Direct record elements / nested BinXML (substitution value type 0x21): omit it + if has_dependency_identifier { + let dependency_identifier = cursor.u16_named("open_start_element_dependency_identifier")?; trace!( "\t Dependency Identifier - `0x{:04x} ({})`", - _dependency_identifier, - _dependency_identifier + dependency_identifier, dependency_identifier ); } - let data_size = try_read!(cursor, u32, "open_start_element_data_size")?; - - // This is a heuristic, sometimes `dependency_identifier` is not present even though it should have been. - // This will result in interpreting garbage bytes as the data size. - // We try to recover from this situation by rolling back the cursor and trying again, without reading the `dependency_identifier`. - if let Some(c) = chunk - && data_size >= c.data.len() as u32 - { - warn!( - "Detected a case where `dependency_identifier` should not have been read. \ - Trying to read again without it." - ); - cursor.seek(SeekFrom::Current(-6)).map_err(|e| { - WrappedIoError::io_error_with_message( - e, - "failed to skip when recovering from `dependency_identifier` hueristic", - cursor, - ) - })?; - return read_open_start_element(cursor, chunk, has_attributes, true); - } + let data_size = cursor.u32_named("open_start_element_data_size")?; trace!("\t Data Size - {}", data_size); - let name = BinXmlNameRef::from_stream(cursor)?; + let name = BinXmlNameRef::from_cursor_with_encoding(cursor, name_encoding)?; trace!("\t Name - {:?}", name); let _attribute_list_data_size = if has_attributes { - try_read!(cursor, u32, "open_start_element_attribute_list_data_size")? + cursor.u32_named("open_start_element_attribute_list_data_size")? } else { 0 }; diff --git a/src/binxml/value_variant.rs b/src/binxml/value_variant.rs index 73807c69..0d6f714b 100644 --- a/src/binxml/value_variant.rs +++ b/src/binxml/value_variant.rs @@ -1,28 +1,22 @@ -use crate::err::{DeserializationError, DeserializationResult as Result, WrappedIoError}; -use encoding::EncodingRef; - -pub use byteorder::{LittleEndian, ReadBytesExt}; - use crate::binxml::deserializer::BinXmlDeserializer; - -use winstructs::guid::Guid; - +use crate::err::{DeserializationError, DeserializationResult as Result}; +use crate::evtx_chunk::EvtxChunk; use crate::model::deserialized::BinXMLDeserializedTokens; -use crate::utils::{ - read_ansi_encoded_string, read_len_prefixed_utf16_string, read_null_terminated_utf16_string, - read_systemtime, read_utf16_by_size, -}; +use crate::utils::ByteCursor; +use crate::utils::invalid_data; +use crate::utils::windows::{filetime_to_datetime, read_sid, read_systime, systime_from_bytes}; + use chrono::{DateTime, Utc}; -use log::trace; -use serde_json::{json, Value}; +use encoding::EncodingRef; +use log::{trace, warn}; +use serde_json::{Value, json}; use std::borrow::Cow; -use std::io::{Cursor, Read, Seek, SeekFrom}; +use std::fmt::Write; +use std::io::Cursor; use std::string::ToString; +use winstructs::guid::Guid; use winstructs::security::Sid; -use crate::evtx_chunk::EvtxChunk; -use std::fmt::Write; - static DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6fZ"; #[derive(Debug, PartialOrd, PartialEq, Clone)] @@ -188,13 +182,13 @@ impl BinXmlValueType { } impl<'a> BinXmlValue<'a> { - pub fn from_binxml_stream( - cursor: &mut Cursor<&'a [u8]>, + pub(crate) fn from_binxml_cursor( + cursor: &mut ByteCursor<'a>, chunk: Option<&'a EvtxChunk<'a>>, size: Option, ansi_codec: EncodingRef, ) -> Result> { - let value_type_token = try_read!(cursor, u8)?; + let value_type_token = cursor.u8()?; let value_type = BinXmlValueType::from_u8(value_type_token).ok_or( DeserializationError::InvalidValueVariant { @@ -203,17 +197,32 @@ impl<'a> BinXmlValue<'a> { }, )?; - let data = Self::deserialize_value_type(&value_type, cursor, chunk, size, ansi_codec)?; + let data = + Self::deserialize_value_type_cursor(&value_type, cursor, chunk, size, ansi_codec)?; Ok(data) } - pub fn deserialize_value_type( - value_type: &BinXmlValueType, + pub fn from_binxml_stream( cursor: &mut Cursor<&'a [u8]>, chunk: Option<&'a EvtxChunk<'a>>, size: Option, ansi_codec: EncodingRef, + ) -> Result> { + let start = cursor.position() as usize; + let buf = *cursor.get_ref(); + let mut c = ByteCursor::with_pos(buf, start)?; + let v = Self::from_binxml_cursor(&mut c, chunk, size, ansi_codec)?; + cursor.set_position(c.position()); + Ok(v) + } + + pub(crate) fn deserialize_value_type_cursor( + value_type: &BinXmlValueType, + cursor: &mut ByteCursor<'a>, + chunk: Option<&'a EvtxChunk<'a>>, + size: Option, + ansi_codec: EncodingRef, ) -> Result> { trace!( "Offset `0x{offset:08x} ({offset}): {value_type:?}, {size:?}", @@ -224,180 +233,278 @@ impl<'a> BinXmlValue<'a> { let value = match (value_type, size) { (BinXmlValueType::NullType, _) => BinXmlValue::NullType, - (BinXmlValueType::StringType, Some(sz)) => BinXmlValue::StringType( - read_utf16_by_size(cursor, u64::from(sz)) - .map_err(|e| { - WrappedIoError::io_error_with_message( - e, - format!("failed to read sized utf-16 string (size `{}`)", sz), - cursor, - ) - })? - .unwrap_or_else(|| "".to_owned()), - ), + + (BinXmlValueType::StringType, Some(sz)) => { + let sz_bytes = usize::from(sz); + let s = if sz_bytes == 0 { + None + } else if !sz_bytes.is_multiple_of(2) { + return Err(invalid_data("sized utf-16 string", cursor.position())); + } else { + cursor.utf16_by_char_count_trimmed(sz_bytes / 2, "")? + }; + BinXmlValue::StringType(s.unwrap_or_else(|| "".to_owned())) + } (BinXmlValueType::StringType, None) => BinXmlValue::StringType( - try_read!(cursor, len_prefixed_utf_16_str, "")?.unwrap_or_default(), + cursor + .len_prefixed_utf16_string(false, "")? + .unwrap_or_default(), ), - (BinXmlValueType::AnsiStringType, Some(sz)) => BinXmlValue::AnsiStringType(Cow::Owned( - read_ansi_encoded_string(cursor, u64::from(sz), ansi_codec)? - .unwrap_or_else(|| "".to_owned()), - )), + + (BinXmlValueType::AnsiStringType, Some(sz)) => { + let sz_bytes = usize::from(sz); + let raw = cursor.take_bytes(sz_bytes, "")?; + let mut data = raw.to_vec(); + data.retain(|&b| b != 0); + let s = ansi_codec + .decode(&data[..], encoding::DecoderTrap::Strict) + .map_err(|m| DeserializationError::AnsiDecodeError { + encoding_used: ansi_codec.name(), + inner_message: m.to_string(), + })?; + BinXmlValue::AnsiStringType(Cow::Owned(s)) + } // AnsiString are always sized according to docs (BinXmlValueType::AnsiStringType, None) => { return Err(DeserializationError::UnimplementedValueVariant { name: "AnsiString".to_owned(), size: None, offset: cursor.position(), - }) - } - (BinXmlValueType::Int8Type, _) => BinXmlValue::Int8Type(try_read!(cursor, i8)?), - (BinXmlValueType::UInt8Type, _) => BinXmlValue::UInt8Type(try_read!(cursor, u8)?), - (BinXmlValueType::Int16Type, _) => BinXmlValue::Int16Type(try_read!(cursor, i16)?), - (BinXmlValueType::UInt16Type, _) => BinXmlValue::UInt16Type(try_read!(cursor, u16)?), - (BinXmlValueType::Int32Type, _) => BinXmlValue::Int32Type(try_read!(cursor, i32)?), - (BinXmlValueType::UInt32Type, _) => BinXmlValue::UInt32Type(try_read!(cursor, u32)?), - (BinXmlValueType::Int64Type, _) => BinXmlValue::Int64Type(try_read!(cursor, i64)?), - (BinXmlValueType::UInt64Type, _) => BinXmlValue::UInt64Type(try_read!(cursor, u64)?), - (BinXmlValueType::Real32Type, _) => BinXmlValue::Real32Type(try_read!(cursor, f32)?), - (BinXmlValueType::Real64Type, _) => BinXmlValue::Real64Type(try_read!(cursor, f64)?), - (BinXmlValueType::BoolType, _) => BinXmlValue::BoolType(try_read!(cursor, bool)?), - (BinXmlValueType::GuidType, _) => BinXmlValue::GuidType(try_read!(cursor, guid)?), + }); + } + + (BinXmlValueType::Int8Type, _) => BinXmlValue::Int8Type(cursor.u8()? as i8), + (BinXmlValueType::UInt8Type, _) => BinXmlValue::UInt8Type(cursor.u8()?), + + (BinXmlValueType::Int16Type, _) => { + BinXmlValue::Int16Type(i16::from_le_bytes(cursor.array::<2>("i16")?)) + } + (BinXmlValueType::UInt16Type, _) => BinXmlValue::UInt16Type(cursor.u16()?), + + (BinXmlValueType::Int32Type, _) => { + BinXmlValue::Int32Type(i32::from_le_bytes(cursor.array::<4>("i32")?)) + } + (BinXmlValueType::UInt32Type, _) => BinXmlValue::UInt32Type(cursor.u32()?), + + (BinXmlValueType::Int64Type, _) => { + BinXmlValue::Int64Type(i64::from_le_bytes(cursor.array::<8>("i64")?)) + } + (BinXmlValueType::UInt64Type, _) => BinXmlValue::UInt64Type(cursor.u64()?), + + (BinXmlValueType::Real32Type, _) => { + BinXmlValue::Real32Type(f32::from_le_bytes(cursor.array::<4>("f32")?)) + } + (BinXmlValueType::Real64Type, _) => { + BinXmlValue::Real64Type(f64::from_le_bytes(cursor.array::<8>("f64")?)) + } + + (BinXmlValueType::BoolType, _) => { + let raw = i32::from_le_bytes(cursor.array::<4>("bool")?); + let v = match raw { + 0 => false, + 1 => true, + other => { + warn!( + "invalid boolean value {} at offset {}; treating as {}", + other, + cursor.position(), + other != 0 + ); + other != 0 + } + }; + BinXmlValue::BoolType(v) + } + + (BinXmlValueType::GuidType, _) => { + let bytes = cursor.take_bytes(16, "guid")?; + let guid = Guid::from_buffer(bytes) + .map_err(|_| invalid_data("guid", cursor.position()))?; + BinXmlValue::GuidType(guid) + } + (BinXmlValueType::SizeTType, Some(4)) => { - BinXmlValue::HexInt32Type(try_read!(cursor, hex32)?) + let v = i32::from_le_bytes(cursor.array::<4>("sizet32")?); + BinXmlValue::HexInt32Type(Cow::Owned(format!("0x{:x}", v))) } (BinXmlValueType::SizeTType, Some(8)) => { - BinXmlValue::HexInt64Type(try_read!(cursor, hex64)?) + let v = i64::from_le_bytes(cursor.array::<8>("sizet64")?); + BinXmlValue::HexInt64Type(Cow::Owned(format!("0x{:x}", v))) } (BinXmlValueType::SizeTType, _) => { return Err(DeserializationError::UnimplementedValueVariant { name: "SizeT".to_owned(), size, offset: cursor.position(), - }) + }); } + (BinXmlValueType::FileTimeType, _) => { - BinXmlValue::FileTimeType(try_read!(cursor, filetime)?) + BinXmlValue::FileTimeType(filetime_to_datetime(cursor.u64()?)) } - (BinXmlValueType::SysTimeType, _) => { - BinXmlValue::SysTimeType(try_read!(cursor, systime)?) - } - (BinXmlValueType::SidType, _) => BinXmlValue::SidType(try_read!(cursor, sid)?), + (BinXmlValueType::SysTimeType, _) => BinXmlValue::SysTimeType(read_systime(cursor)?), + (BinXmlValueType::SidType, _) => BinXmlValue::SidType(read_sid(cursor)?), + (BinXmlValueType::HexInt32Type, _) => { - BinXmlValue::HexInt32Type(try_read!(cursor, hex32)?) + let v = i32::from_le_bytes(cursor.array::<4>("hex32")?); + BinXmlValue::HexInt32Type(Cow::Owned(format!("0x{:x}", v))) } (BinXmlValueType::HexInt64Type, _) => { - BinXmlValue::HexInt64Type(try_read!(cursor, hex64)?) + let v = i64::from_le_bytes(cursor.array::<8>("hex64")?); + BinXmlValue::HexInt64Type(Cow::Owned(format!("0x{:x}", v))) } - (BinXmlValueType::BinXmlType, None) => { - let tokens = BinXmlDeserializer::read_binxml_fragment( - cursor, chunk, None, true, ansi_codec, - )?; - BinXmlValue::BinXmlType(tokens) - } - (BinXmlValueType::BinXmlType, Some(sz)) => { + (BinXmlValueType::BinXmlType, size) => { + let data_size = size.map(u32::from); + let start_pos = cursor.position(); + let mut c = Cursor::new(cursor.buf()); + c.set_position(start_pos); let tokens = BinXmlDeserializer::read_binxml_fragment( - cursor, - chunk, - Some(u32::from(sz)), - true, - ansi_codec, + &mut c, chunk, data_size, false, ansi_codec, )?; - + cursor.set_pos_u64(c.position(), "advance after BinXmlType")?; BinXmlValue::BinXmlType(tokens) } - (BinXmlValueType::BinaryType, Some(sz)) => { - // Borrow the underlying data from the cursor, and return a ref to it. - let data = *cursor.get_ref(); - let bytes = - &data[cursor.position() as usize..(cursor.position() + u64::from(sz)) as usize]; - - cursor.seek(SeekFrom::Current(i64::from(sz))).map_err(|e| { - WrappedIoError::io_error_with_message( - e, - "failed to read binary value_variant", - cursor, - ) - })?; + (BinXmlValueType::BinaryType, Some(sz)) => { + let bytes = cursor.take_bytes(usize::from(sz), "binary")?; BinXmlValue::BinaryType(bytes) } - // The array types are always sized. - (BinXmlValueType::StringArrayType, Some(sz)) => BinXmlValue::StringArrayType( - try_read_sized_array!(cursor, null_terminated_utf_16_str, sz), - ), - (BinXmlValueType::Int8ArrayType, Some(sz)) => { - BinXmlValue::Int8ArrayType(try_read_sized_array!(cursor, i8, sz)) - } - (BinXmlValueType::UInt8ArrayType, Some(sz)) => { - let mut data = vec![0; sz as usize]; - cursor.read_exact(&mut data).map_err(|e| { - WrappedIoError::io_error_with_message( - e, - "Failed to read `UInt8ArrayType`", - cursor, - ) - })?; - BinXmlValue::UInt8ArrayType(data) - } - (BinXmlValueType::Int16ArrayType, Some(sz)) => { - BinXmlValue::Int16ArrayType(try_read_sized_array!(cursor, i16, sz)) - } - (BinXmlValueType::UInt16ArrayType, Some(sz)) => { - BinXmlValue::UInt16ArrayType(try_read_sized_array!(cursor, u16, sz)) - } - (BinXmlValueType::Int32ArrayType, Some(sz)) => { - BinXmlValue::Int32ArrayType(try_read_sized_array!(cursor, i32, sz)) - } - (BinXmlValueType::UInt32ArrayType, Some(sz)) => { - BinXmlValue::UInt32ArrayType(try_read_sized_array!(cursor, u32, sz)) - } - (BinXmlValueType::Int64ArrayType, Some(sz)) => { - BinXmlValue::Int64ArrayType(try_read_sized_array!(cursor, i64, sz)) - } - (BinXmlValueType::UInt64ArrayType, Some(sz)) => { - BinXmlValue::UInt64ArrayType(try_read_sized_array!(cursor, u64, sz)) - } - (BinXmlValueType::Real32ArrayType, Some(sz)) => { - BinXmlValue::Real32ArrayType(try_read_sized_array!(cursor, f32, sz)) - } - (BinXmlValueType::Real64ArrayType, Some(sz)) => { - BinXmlValue::Real64ArrayType(try_read_sized_array!(cursor, f64, sz)) - } - (BinXmlValueType::BoolArrayType, Some(sz)) => { - BinXmlValue::BoolArrayType(try_read_sized_array!(cursor, bool, sz)) - } - (BinXmlValueType::GuidArrayType, Some(sz)) => { - BinXmlValue::GuidArrayType(try_read_sized_array!(cursor, guid, sz)) - } - (BinXmlValueType::FileTimeArrayType, Some(sz)) => { - BinXmlValue::FileTimeArrayType(try_read_sized_array!(cursor, filetime, sz)) + // The array types are always sized. + (BinXmlValueType::StringArrayType, Some(sz)) => { + let size_usize = usize::from(sz); + let start = cursor.pos(); + let end = start.saturating_add(size_usize); + let mut out: Vec = Vec::new(); + while cursor.pos() < end { + out.push(cursor.null_terminated_utf16_string("string_array")?); + } + BinXmlValue::StringArrayType(out) } - (BinXmlValueType::SysTimeArrayType, Some(sz)) => { - BinXmlValue::SysTimeArrayType(try_read_sized_array!(cursor, systime, sz)) + (BinXmlValueType::Int8ArrayType, Some(sz)) => { + let bytes = cursor.take_bytes(usize::from(sz), "i8_array")?; + BinXmlValue::Int8ArrayType(bytes.iter().map(|&b| b as i8).collect()) } + (BinXmlValueType::UInt8ArrayType, Some(sz)) => BinXmlValue::UInt8ArrayType( + cursor.take_bytes(usize::from(sz), "u8_array")?.to_vec(), + ), + (BinXmlValueType::Int16ArrayType, Some(sz)) => BinXmlValue::Int16ArrayType( + cursor.read_sized_vec_aligned::<2, _>(sz, "i16_array", |_off, b| { + Ok(i16::from_le_bytes(*b)) + })?, + ), + (BinXmlValueType::UInt16ArrayType, Some(sz)) => BinXmlValue::UInt16ArrayType( + cursor.read_sized_vec_aligned::<2, _>(sz, "u16_array", |_off, b| { + Ok(u16::from_le_bytes(*b)) + })?, + ), + (BinXmlValueType::Int32ArrayType, Some(sz)) => BinXmlValue::Int32ArrayType( + cursor.read_sized_vec_aligned::<4, _>(sz, "i32_array", |_off, b| { + Ok(i32::from_le_bytes(*b)) + })?, + ), + (BinXmlValueType::UInt32ArrayType, Some(sz)) => BinXmlValue::UInt32ArrayType( + cursor.read_sized_vec_aligned::<4, _>(sz, "u32_array", |_off, b| { + Ok(u32::from_le_bytes(*b)) + })?, + ), + (BinXmlValueType::Int64ArrayType, Some(sz)) => BinXmlValue::Int64ArrayType( + cursor.read_sized_vec_aligned::<8, _>(sz, "i64_array", |_off, b| { + Ok(i64::from_le_bytes(*b)) + })?, + ), + (BinXmlValueType::UInt64ArrayType, Some(sz)) => BinXmlValue::UInt64ArrayType( + cursor.read_sized_vec_aligned::<8, _>(sz, "u64_array", |_off, b| { + Ok(u64::from_le_bytes(*b)) + })?, + ), + (BinXmlValueType::Real32ArrayType, Some(sz)) => BinXmlValue::Real32ArrayType( + cursor.read_sized_vec_aligned::<4, _>(sz, "f32_array", |_off, b| { + Ok(f32::from_le_bytes(*b)) + })?, + ), + (BinXmlValueType::Real64ArrayType, Some(sz)) => BinXmlValue::Real64ArrayType( + cursor.read_sized_vec_aligned::<8, _>(sz, "f64_array", |_off, b| { + Ok(f64::from_le_bytes(*b)) + })?, + ), + (BinXmlValueType::BoolArrayType, Some(sz)) => BinXmlValue::BoolArrayType( + cursor.read_sized_vec_aligned::<4, _>(sz, "bool_array", |off, b| { + let raw = i32::from_le_bytes(*b); + Ok(match raw { + 0 => false, + 1 => true, + other => { + warn!( + "invalid boolean value {} at offset {}; treating as {}", + other, + off, + other != 0 + ); + other != 0 + } + }) + })?, + ), + (BinXmlValueType::GuidArrayType, Some(sz)) => BinXmlValue::GuidArrayType( + cursor.read_sized_vec_aligned::<16, _>(sz, "guid_array", |off, b| { + Guid::from_buffer(b).map_err(|_| invalid_data("guid", off)) + })?, + ), + (BinXmlValueType::FileTimeArrayType, Some(sz)) => BinXmlValue::FileTimeArrayType( + cursor.read_sized_vec_aligned::<8, _>(sz, "filetime_array", |_off, b| { + Ok(filetime_to_datetime(u64::from_le_bytes(*b))) + })?, + ), + (BinXmlValueType::SysTimeArrayType, Some(sz)) => BinXmlValue::SysTimeArrayType( + cursor.read_sized_vec_aligned::<16, _>(sz, "systime_array", |_off, b| { + systime_from_bytes(b) + })?, + ), (BinXmlValueType::SidArrayType, Some(sz)) => { - BinXmlValue::SidArrayType(try_read_sized_array!(cursor, sid, sz)) - } - (BinXmlValueType::HexInt32ArrayType, Some(sz)) => { - BinXmlValue::HexInt32ArrayType(try_read_sized_array!(cursor, hex32, sz)) - } - (BinXmlValueType::HexInt64ArrayType, Some(sz)) => { - BinXmlValue::HexInt64ArrayType(try_read_sized_array!(cursor, hex64, sz)) - } + // SID size is variable; we can only preallocate with a heuristic. + BinXmlValue::SidArrayType(cursor.read_sized_vec(sz, 8, |c| read_sid(c))?) + } + (BinXmlValueType::HexInt32ArrayType, Some(sz)) => BinXmlValue::HexInt32ArrayType( + cursor.read_sized_vec_aligned::<4, _>(sz, "hex32_array", |_off, b| { + let v = i32::from_le_bytes(*b); + Ok(Cow::Owned(format!("0x{:x}", v))) + })?, + ), + (BinXmlValueType::HexInt64ArrayType, Some(sz)) => BinXmlValue::HexInt64ArrayType( + cursor.read_sized_vec_aligned::<8, _>(sz, "hex64_array", |_off, b| { + let v = i64::from_le_bytes(*b); + Ok(Cow::Owned(format!("0x{:x}", v))) + })?, + ), _ => { return Err(DeserializationError::UnimplementedValueVariant { name: format!("{:?}", value_type), size, offset: cursor.position(), - }) + }); } }; Ok(value) } + + pub fn deserialize_value_type( + value_type: &BinXmlValueType, + cursor: &mut Cursor<&'a [u8]>, + chunk: Option<&'a EvtxChunk<'a>>, + size: Option, + ansi_codec: EncodingRef, + ) -> Result> { + let start = cursor.position() as usize; + let buf = *cursor.get_ref(); + let mut c = ByteCursor::with_pos(buf, start)?; + let v = Self::deserialize_value_type_cursor(value_type, &mut c, chunk, size, ansi_codec)?; + cursor.set_position(c.position()); + Ok(v) + } } fn to_delimited_list(ns: impl AsRef>) -> String { @@ -426,10 +533,14 @@ impl<'c> From> for serde_json::Value { BinXmlValue::Real64Type(num) => json!(num), BinXmlValue::BoolType(num) => json!(num), BinXmlValue::BinaryType(bytes) => { - json!(bytes.iter().fold(String::with_capacity(bytes.len() * 2), |mut acc, &b| { - write!(acc, "{:02X}", b).unwrap(); - acc - })) + json!( + bytes + .iter() + .fold(String::with_capacity(bytes.len() * 2), |mut acc, &b| { + write!(acc, "{:02X}", b).unwrap(); + acc + }) + ) } BinXmlValue::GuidType(guid) => json!(guid.to_string()), // BinXmlValue::SizeTType(sz) => json!(sz.to_string()), @@ -490,10 +601,14 @@ impl<'c> From<&'c BinXmlValue<'c>> for serde_json::Value { BinXmlValue::Real64Type(num) => json!(num), BinXmlValue::BoolType(num) => json!(num), BinXmlValue::BinaryType(bytes) => { - json!(bytes.iter().fold(String::with_capacity(bytes.len() * 2), |mut acc, &b| { - write!(acc, "{:02X}", b).unwrap(); - acc - })) + json!( + bytes + .iter() + .fold(String::with_capacity(bytes.len() * 2), |mut acc, &b| { + write!(acc, "{:02X}", b).unwrap(); + acc + }) + ) } BinXmlValue::GuidType(guid) => json!(guid.to_string()), // BinXmlValue::SizeTType(sz) => json!(sz.to_string()), @@ -553,12 +668,13 @@ impl BinXmlValue<'_> { BinXmlValue::Real32Type(num) => Cow::Owned(num.to_string()), BinXmlValue::Real64Type(num) => Cow::Owned(num.to_string()), BinXmlValue::BoolType(num) => Cow::Owned(num.to_string()), - BinXmlValue::BinaryType(bytes) => { - Cow::Owned(bytes.iter().fold(String::with_capacity(bytes.len() * 2), |mut acc, &b| { + BinXmlValue::BinaryType(bytes) => Cow::Owned(bytes.iter().fold( + String::with_capacity(bytes.len() * 2), + |mut acc, &b| { write!(acc, "{:02X}", b).unwrap(); acc - })) - } + }, + )), BinXmlValue::GuidType(guid) => Cow::Owned(guid.to_string()), BinXmlValue::SizeTType(sz) => Cow::Owned(sz.to_string()), BinXmlValue::FileTimeType(tm) => Cow::Owned(tm.format(DATETIME_FORMAT).to_string()), diff --git a/src/err.rs b/src/err.rs index 4327703a..1eb42ccc 100644 --- a/src/err.rs +++ b/src/err.rs @@ -99,11 +99,11 @@ pub enum DeserializationError { source: WrappedIoError, }, - #[error("An expected I/O error has occurred")] - UnexpectedIoError(#[from] WrappedIoError), + #[error(transparent)] + IoWithContext(#[from] WrappedIoError), - #[error("An expected I/O error has occurred")] - RemoveMe(#[from] io::Error), + #[error(transparent)] + Io(#[from] io::Error), /// An extra layer of error indirection to keep template GUID. #[error("Failed to deserialize template `{template_id}`")] @@ -127,6 +127,26 @@ pub enum DeserializationError { )] InvalidValueVariant { value: u8, offset: u64 }, + #[error("buffer too small for {what} at offset {offset} (need {need} bytes, have {have})")] + Truncated { + what: &'static str, + offset: u64, + need: usize, + have: usize, + }, + + #[error( + "Offset 0x{offset:08x}: WEVT inline name hash mismatch (expected 0x{expected:04x}, found 0x{found:04x})" + )] + WevtInlineNameHashMismatch { + expected: u16, + found: u16, + offset: u64, + }, + + #[error("Offset 0x{offset:08x}: WEVT inline name missing NUL terminator (found 0x{found:04x})")] + WevtInlineNameMissingNulTerminator { found: u16, offset: u64 }, + #[error("An out-of-range date, invalid month and/or day")] InvalidDateTimeError, diff --git a/src/evtx_chunk.rs b/src/evtx_chunk.rs index ecb981f5..2121ebf6 100644 --- a/src/evtx_chunk.rs +++ b/src/evtx_chunk.rs @@ -2,20 +2,17 @@ use crate::err::{ ChunkError, DeserializationError, DeserializationResult, EvtxChunkResult, EvtxError, }; -use crate::evtx_record::{EvtxRecord, EvtxRecordHeader}; +use crate::evtx_record::{EVTX_RECORD_HEADER_SIZE, EvtxRecord, EvtxRecordHeader}; +use crate::utils::bytes; use log::{debug, info, trace}; -use std::{ - io::Cursor, - io::{Read, Seek, SeekFrom}, -}; +use std::io::Cursor; use crate::binxml::deserializer::BinXmlDeserializer; use crate::string_cache::StringCache; use crate::template_cache::TemplateCache; use crate::{ParserSettings, checksum_ieee}; -use byteorder::{LittleEndian, ReadBytesExt}; use std::sync::Arc; const EVTX_CHUNK_HEADER_SIZE: usize = 512; @@ -65,8 +62,7 @@ impl EvtxChunkData { /// Construct a new chunk from the given data. /// Note that even when validate_checksum is set to false, the header magic is still checked. pub fn new(data: Vec, validate_checksum: bool) -> EvtxChunkResult { - let mut cursor = Cursor::new(data.as_slice()); - let header = EvtxChunkHeader::from_reader(&mut cursor)?; + let header = EvtxChunkHeader::from_bytes(&data)?; let chunk = EvtxChunkData { header, data }; if validate_checksum && !chunk.validate_checksum() { @@ -248,45 +244,50 @@ impl<'a> Iterator for IterChunkRecords<'a> { return None; } - let start = self.offset_from_chunk_start as usize; - if start >= self.chunk.data.len() { + let record_start = self.offset_from_chunk_start; + let record_start_usize = record_start as usize; + + if record_start_usize >= self.chunk.data.len() { // Avoid panicking on an out-of-bounds slice if the header is corrupted. self.exhausted = true; return None; } - let remaining = &self.chunk.data[start..]; - if remaining.len() < 4 { + if self.chunk.data.len() - record_start_usize < 4 { // Not enough bytes for the record header magic, treat as end-of-chunk. self.exhausted = true; return None; } - let mut cursor = Cursor::new(remaining); + let record_header = + match EvtxRecordHeader::from_bytes_at(self.chunk.data, record_start_usize) { + Ok(record_header) => record_header, + Err(DeserializationError::InvalidEvtxRecordHeaderMagic { magic }) => { + // Some producers write incorrect `free_space_offset` / `last_event_record_id`. + // In such cases we may attempt to parse the chunk slack area, which is typically + // zero-padded. Treat an all-zero "magic" as a clean end-of-chunk instead of + // emitting an error (see issue #197). + if magic == [0, 0, 0, 0] { + self.exhausted = true; + return None; + } - let record_header = match EvtxRecordHeader::from_reader(&mut cursor) { - Ok(record_header) => record_header, - Err(DeserializationError::InvalidEvtxRecordHeaderMagic { magic }) => { - // Some producers write incorrect `free_space_offset` / `last_event_record_id`. - // In such cases we may attempt to parse the chunk slack area, which is typically - // zero-padded. Treat an all-zero "magic" as a clean end-of-chunk instead of - // emitting an error (see issue #197). - if magic == [0, 0, 0, 0] { + self.exhausted = true; + return Some(Err(EvtxError::DeserializationError( + DeserializationError::InvalidEvtxRecordHeaderMagic { magic }, + ))); + } + Err(DeserializationError::Truncated { .. }) => { + // Truncated record header near the end-of-chunk: treat as clean end-of-chunk. self.exhausted = true; return None; } - - self.exhausted = true; - return Some(Err(EvtxError::DeserializationError( - DeserializationError::InvalidEvtxRecordHeaderMagic { magic }, - ))); - } - Err(err) => { - // We currently do not try to recover after an invalid record. - self.exhausted = true; - return Some(Err(EvtxError::DeserializationError(err))); - } - }; + Err(err) => { + // We currently do not try to recover after an invalid record. + self.exhausted = true; + return Some(Err(EvtxError::DeserializationError(err))); + } + }; info!("Record id - {}", record_header.event_record_id); debug!("Record header - {:?}", record_header); @@ -308,7 +309,7 @@ impl<'a> Iterator for IterChunkRecords<'a> { // We avoid creating new references so that `BinXmlDeserializer` can still generate 'a data. let deserializer = BinXmlDeserializer::init( self.chunk.data, - self.offset_from_chunk_start + cursor.position(), + record_start + EVTX_RECORD_HEADER_SIZE as u64, Some(self.chunk), false, self.settings.get_ansi_codec(), @@ -355,37 +356,38 @@ impl<'a> Iterator for IterChunkRecords<'a> { } impl EvtxChunkHeader { - pub fn from_reader(input: &mut Cursor<&[u8]>) -> DeserializationResult { - let mut magic = [0_u8; 8]; - input.take(8).read_exact(&mut magic)?; + pub fn from_bytes(data: &[u8]) -> DeserializationResult { + // We only parse the fixed header prefix; the rest of the chunk may be shorter in some + // corrupted cases, but the header itself must be present. + 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 = try_read!(input, u64)?; - let last_event_record_number = try_read!(input, u64)?; - let first_event_record_id = try_read!(input, u64)?; - let last_event_record_id = try_read!(input, u64)?; - - let header_size = try_read!(input, u32)?; - let last_event_record_data_offset = try_read!(input, u32)?; - let free_space_offset = try_read!(input, u32)?; - let events_checksum = try_read!(input, u32)?; + 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")?; - // Reserved - input.seek(SeekFrom::Current(64))?; + 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 = try_read!(input, u32)?; + let raw_flags = bytes::read_u32_le_r(data, 120, "chunk.flags")?; let flags = ChunkFlags::from_bits_truncate(raw_flags); - let header_chunk_checksum = try_read!(input, u32)?; + let header_chunk_checksum = bytes::read_u32_le_r(data, 124, "chunk.header_chunk_checksum")?; - let mut strings_offsets = vec![0_u32; 64]; - input.read_u32_into::(&mut strings_offsets)?; - - let mut template_offsets = vec![0_u32; 32]; - input.read_u32_into::(&mut template_offsets)?; + // Offsets arrays: fixed sizes (64 + 32 u32s). + 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, @@ -402,6 +404,16 @@ impl EvtxChunkHeader { strings_offsets, }) } + + pub fn from_reader(input: &mut Cursor<&[u8]>) -> DeserializationResult { + 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)] diff --git a/src/evtx_file_header.rs b/src/evtx_file_header.rs index f345e8b6..9b8a9438 100644 --- a/src/evtx_file_header.rs +++ b/src/evtx_file_header.rs @@ -1,7 +1,7 @@ use crate::err::{DeserializationError, DeserializationResult, WrappedIoError}; +use crate::utils::bytes; -use byteorder::ReadBytesExt; -use std::io::{Read, Seek, SeekFrom}; +use std::io::{Read, Seek}; #[derive(Debug, PartialEq, Eq)] pub struct EvtxFileHeader { @@ -29,51 +29,53 @@ bitflags! { } impl EvtxFileHeader { - pub fn from_stream(stream: &mut T) -> DeserializationResult { - let mut magic = [0_u8; 8]; - stream.take(8).read_exact(&mut magic).map_err(|e| { - WrappedIoError::io_error_with_message(e, "failed to read file_header magic", stream) - })?; + pub fn from_bytes(data: &[u8]) -> DeserializationResult { + // We only need the fixed 128-byte header prefix (the full header block is 4096 bytes). + let _ = bytes::slice_r(data, 0, 128, "EVTX file header")?; + let magic = bytes::read_array_r::<8>(data, 0, "file header magic")?; if &magic != b"ElfFile\x00" { return Err(DeserializationError::InvalidEvtxFileHeaderMagic { magic }); } - let oldest_chunk = try_read!(stream, u64, "file_header_oldest_chunk")?; - let current_chunk_num = try_read!(stream, u64, "file_header_current_chunk_num")?; - let next_record_num = try_read!(stream, u64, "file_header_next_record_num")?; - let header_size = try_read!(stream, u32, "file_header_header_size")?; - let minor_version = try_read!(stream, u16, "file_header_minor_version")?; - let major_version = try_read!(stream, u16, "file_header_major_version")?; - let header_block_size = try_read!(stream, u16, "file_header_header_block_size")?; - let chunk_count = try_read!(stream, u16, "file_header_chunk_count")?; - - // unused - stream.seek(SeekFrom::Current(76)).map_err(|e| { - WrappedIoError::io_error_with_message(e, "failed to seek in file_header", stream) - })?; + let oldest_chunk = bytes::read_u64_le_r(data, 8, "file_header_oldest_chunk")?; + let current_chunk_num = bytes::read_u64_le_r(data, 16, "file_header_current_chunk_num")?; + let next_record_num = bytes::read_u64_le_r(data, 24, "file_header_next_record_num")?; + let header_size = bytes::read_u32_le_r(data, 32, "file_header_header_size")?; + let minor_version = bytes::read_u16_le_r(data, 36, "file_header_minor_version")?; + let major_version = bytes::read_u16_le_r(data, 38, "file_header_major_version")?; + let header_block_size = bytes::read_u16_le_r(data, 40, "file_header_header_block_size")?; + let chunk_count = bytes::read_u16_le_r(data, 42, "file_header_chunk_count")?; - let raw_flags = try_read!(stream, u32, "file_header_flags")?; + let raw_flags = bytes::read_u32_le_r(data, 120, "file_header_flags")?; let flags = HeaderFlags::from_bits_truncate(raw_flags); - let checksum = try_read!(stream, u32, "file_header_checksum")?; - // unused - stream.seek(SeekFrom::Current(4096 - 128)).map_err(|e| { - WrappedIoError::io_error_with_message(e, "failed to seek in file_header", stream) - })?; + let checksum = bytes::read_u32_le_r(data, 124, "file_header_checksum")?; Ok(EvtxFileHeader { first_chunk_number: oldest_chunk, last_chunk_number: current_chunk_num, next_record_id: next_record_num, - header_block_size, + header_size, minor_version, major_version, - header_size, + header_block_size, chunk_count, flags, checksum, }) } + + pub fn from_stream(stream: &mut T) -> DeserializationResult { + let mut header_block = [0_u8; crate::evtx_parser::EVTX_FILE_HEADER_SIZE]; + stream.read_exact(&mut header_block).map_err(|e| { + WrappedIoError::io_error_with_message( + e, + "failed to read EVTX file header block", + stream, + ) + })?; + Self::from_bytes(&header_block) + } } #[cfg(test)] diff --git a/src/evtx_record.rs b/src/evtx_record.rs index 60b4ff94..aef3fd1a 100644 --- a/src/evtx_record.rs +++ b/src/evtx_record.rs @@ -4,16 +4,19 @@ use crate::err::{ }; use crate::json_output::JsonOutput; use crate::model::deserialized::BinXMLDeserializedTokens; +use crate::utils::bytes; +use crate::utils::windows::filetime_to_datetime; use crate::xml_output::{BinXmlOutput, XmlOutput}; use crate::{EvtxChunk, ParserSettings}; -use byteorder::ReadBytesExt; use chrono::prelude::*; -use std::io::{Cursor, Read}; +use std::io::Cursor; use std::sync::Arc; pub type RecordId = u64; +pub(crate) const EVTX_RECORD_HEADER_SIZE: usize = 24; + #[derive(Debug, Clone)] pub struct EvtxRecord<'a> { pub chunk: &'a EvtxChunk<'a>, @@ -38,17 +41,19 @@ pub struct SerializedEvtxRecord { } impl EvtxRecordHeader { - pub fn from_reader(input: &mut Cursor<&[u8]>) -> DeserializationResult { - let mut magic = [0_u8; 4]; - input.take(4).read_exact(&mut magic)?; + pub fn from_bytes_at(buf: &[u8], offset: usize) -> DeserializationResult { + let _ = bytes::slice_r(buf, offset, EVTX_RECORD_HEADER_SIZE, "EVTX record header")?; + let magic = bytes::read_array_r::<4>(buf, offset, "record header magic")?; if &magic != b"\x2a\x2a\x00\x00" { return Err(DeserializationError::InvalidEvtxRecordHeaderMagic { magic }); } - let size = try_read!(input, u32)?; - let record_id = try_read!(input, u64)?; - let timestamp = try_read!(input, filetime)?; + let size = bytes::read_u32_le_r(buf, offset + 4, "record.data_size")?; + let record_id = bytes::read_u64_le_r(buf, offset + 8, "record.event_record_id")?; + let filetime = bytes::read_u64_le_r(buf, offset + 16, "record.filetime")?; + + let timestamp = filetime_to_datetime(filetime); Ok(EvtxRecordHeader { data_size: size, @@ -57,10 +62,22 @@ impl EvtxRecordHeader { }) } + pub fn from_bytes(buf: &[u8]) -> DeserializationResult { + Self::from_bytes_at(buf, 0) + } + + pub fn from_reader(input: &mut Cursor<&[u8]>) -> DeserializationResult { + let start = input.position() as usize; + let buf = input.get_ref(); + let header = Self::from_bytes_at(buf, start)?; + input.set_position((start + EVTX_RECORD_HEADER_SIZE) as u64); + Ok(header) + } + pub fn record_data_size(&self) -> Result { // 24 - record header size // 4 - copy of size record size - let decal = 24 + 4; + let decal = EVTX_RECORD_HEADER_SIZE as u32 + 4; if self.data_size < decal { return Err(EvtxError::InvalidDataSize { length: self.data_size, diff --git a/src/lib.rs b/src/lib.rs index 6a69b2e4..e0c8a8e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,10 +3,6 @@ #![allow(clippy::upper_case_acronyms)] // Don't allow dbg! prints in release. #![cfg_attr(not(debug_assertions), deny(clippy::dbg_macro))] -// This needs to come first! -#[macro_use] -mod macros; - #[macro_use] extern crate bitflags; @@ -22,6 +18,10 @@ pub mod binxml; pub mod err; pub mod model; +// Optional: PE resource parsing to extract WEVT_TEMPLATE blobs (see issue #103). +#[cfg(feature = "wevt_templates")] +pub mod wevt_templates; + mod evtx_chunk; mod evtx_file_header; mod evtx_parser; diff --git a/src/macros.rs b/src/macros.rs deleted file mode 100644 index 4b87090b..00000000 --- a/src/macros.rs +++ /dev/null @@ -1,225 +0,0 @@ -macro_rules! capture_context { - ($cursor: ident, $e: ident, $name: expr_2021) => {{ - let inner = $crate::err::WrappedIoError::capture_hexdump(Box::new($e), $cursor); - $crate::err::DeserializationError::from(inner) - }}; - ($cursor: ident, $e: ident, $token: expr_2021, $name: expr_2021) => {{ - let inner = $crate::err::WrappedIoError::capture_hexdump(Box::new($e), $cursor); - $crate::err::DeserializationError::FailedToReadToken { - t: $token.to_owned(), - token_name: $name, - source: inner, - } - }}; -} - -macro_rules! try_seek { - ($cursor: ident, $offset: expr_2021, $name: expr_2021) => { - $cursor - .seek(SeekFrom::Start(u64::from($offset.clone()))) - .map_err(|e| capture_context!($cursor, e, $name)) - }; -} - -/// Tries to read X bytes from the cursor, if reading fails, captures position nicely. -macro_rules! try_read { - ($cursor: ident, u8, $name: expr_2021) => { - $cursor - .read_u8() - .map_err(|e| capture_context!($cursor, e, "u8", $name)) - }; - - ($cursor: ident, u8) => { - try_read!($cursor, u8, "") - }; - - ($cursor: ident, i8, $name: expr_2021) => { - $cursor - .read_i8() - .map_err(|e| capture_context!($cursor, e, "i8", $name)) - }; - - ($cursor: ident, i8) => { - try_read!($cursor, i8, "") - }; - - ($cursor: ident, u16, $name: expr_2021) => { - $cursor - .read_u16::() - .map_err(|e| capture_context!($cursor, e, "u16", $name)) - }; - - ($cursor: ident, u16) => { - try_read!($cursor, u16, "") - }; - - ($cursor: ident, i16, $name: expr_2021) => { - $cursor - .read_i16::() - .map_err(|e| capture_context!($cursor, e, "i16", $name)) - }; - - ($cursor: ident, i16) => { - try_read!($cursor, i16, "") - }; - - ($cursor: ident, i32, $name: expr_2021) => { - $cursor - .read_i32::() - .map_err(|e| capture_context!($cursor, e, "i32", $name)) - }; - - ($cursor: ident, i32) => { - try_read!($cursor, i32, "") - }; - - ($cursor: ident, u32, $name: expr_2021) => { - $cursor - .read_u32::() - .map_err(|e| capture_context!($cursor, e, "u32", $name)) - }; - - ($cursor: ident, u32) => { - try_read!($cursor, u32, "") - }; - - ($cursor: ident, f32, $name: expr_2021) => { - $cursor - .read_f32::() - .map_err(|e| capture_context!($cursor, e, "f32", $name)) - }; - - ($cursor: ident, f32) => { - try_read!($cursor, f32, "") - }; - - ($cursor: ident, i64, $name: expr_2021) => { - $cursor - .read_i64::() - .map_err(|e| capture_context!($cursor, e, "i64", $name)) - }; - - ($cursor: ident, i64) => { - try_read!($cursor, i64, "") - }; - - ($cursor: ident, u64, $name: expr_2021) => { - $cursor - .read_u64::() - .map_err(|e| capture_context!($cursor, e, "u64", $name)) - }; - - ($cursor: ident, u64) => { - try_read!($cursor, u64, "") - }; - - ($cursor: ident, f64, $name: expr_2021) => { - $cursor - .read_f64::() - .map_err(|e| capture_context!($cursor, e, "f64", $name)) - }; - - ($cursor: ident, f64) => { - try_read!($cursor, f64, "") - }; - - ($cursor: ident, bool) => {{ - let bool_value = try_read!($cursor, i32); - match bool_value { - Ok(0) => Ok(false), - Ok(1) => Ok(true), - Ok(number) => { - log::warn!( - "{:} is an unknown value for bool, coercing to `true`", - number - ); - Ok(true) - } - Err(e) => Err(e), - } - }}; - - ($cursor: ident, guid) => { - try_read!($cursor, guid, "") - }; - - ($cursor: ident, guid, $name: expr_2021) => { - Guid::from_reader($cursor).map_err(|e| capture_context!($cursor, e, "guid", $name)) - }; - - ($cursor: ident, len_prefixed_utf_16_str) => {{ - try_read!($cursor, len_prefixed_utf_16_str, "") - }}; - - ($cursor: ident, len_prefixed_utf_16_str, $name: expr_2021) => { - read_len_prefixed_utf16_string($cursor, false) - .map_err(|e| capture_context!($cursor, e, "len_prefixed_utf_16_str", $name)) - }; - - ($cursor: ident, len_prefixed_utf_16_str_nul_terminated) => {{ - try_read!($cursor, len_prefixed_utf_16_str_nul_terminated, "") - }}; - - ($cursor: ident, len_prefixed_utf_16_str_nul_terminated, $name: expr_2021) => { - read_len_prefixed_utf16_string($cursor, true).map_err(|e| { - capture_context!($cursor, e, "len_prefixed_utf_16_str_nul_terminated", $name) - }) - }; - - ($cursor: ident, null_terminated_utf_16_str) => {{ - try_read!($cursor, null_terminated_utf_16_str, "") - }}; - - ($cursor: ident, null_terminated_utf_16_str, $name: expr_2021) => { - read_null_terminated_utf16_string($cursor) - .map_err(|e| capture_context!($cursor, e, "null_terminated_utf_16_str", $name)) - }; - - ($cursor: ident, sid, $name: expr_2021) => { - Sid::from_reader($cursor).map_err(|e| capture_context!($cursor, e, "ntsid", $name)) - }; - - ($cursor: ident, sid) => { - try_read!($cursor, sid, "") - }; - - ($cursor: ident, hex32) => {{ - try_read!($cursor, i32).map(|value| Cow::Owned(format!("0x{:x}", value))) - }}; - - ($cursor: ident, hex64) => { - try_read!($cursor, i64).map(|value| Cow::Owned(format!("0x{:x}", value))) - }; - - ($cursor: ident, filetime) => { - try_read!($cursor, filetime, "") - }; - - ($cursor: ident, filetime, $name: expr_2021) => { - winstructs::timestamp::WinTimestamp::from_reader($cursor) - .map_err(|e| capture_context!($cursor, e, "filetime", $name)) - .map(|t| t.to_datetime()) - }; - - ($cursor: ident, systime) => { - read_systemtime($cursor) - }; -} - -macro_rules! try_read_sized_array { - ($cursor: ident, $unit: ident, $size: ident) => {{ - let mut array = vec![]; - let start_pos = $cursor.position(); - - loop { - if ($cursor.position() - start_pos) >= u64::from($size) { - break; - } - - let val = try_read!($cursor, $unit)?; - array.push(val); - } - - array - }}; -} diff --git a/src/model/deserialized.rs b/src/model/deserialized.rs index b674e4bc..678d6bdb 100644 --- a/src/model/deserialized.rs +++ b/src/model/deserialized.rs @@ -69,7 +69,12 @@ pub struct BinXmlEntityReference { #[derive(Debug, PartialOrd, PartialEq, Clone)] pub struct BinXmlTemplateRef<'a> { + pub template_id: u32, pub template_def_offset: ChunkOffset, + /// When the template definition header is embedded inline in the record's TemplateInstance, + /// we can read the template GUID directly. Otherwise, the GUID lives in the template + /// definition referenced by `template_def_offset` (typically in the chunk template table). + pub template_guid: Option, pub substitution_array: Vec>, } diff --git a/src/string_cache.rs b/src/string_cache.rs index 62a246d3..307deed1 100644 --- a/src/string_cache.rs +++ b/src/string_cache.rs @@ -1,11 +1,10 @@ use crate::ChunkOffset; use crate::binxml::name::{BinXmlName, BinXmlNameLink}; use crate::err::DeserializationResult; +use crate::utils::ByteCursor; use log::trace; -use std::borrow::BorrowMut; use std::collections::HashMap; -use std::io::{Cursor, Seek, SeekFrom}; #[derive(Debug)] pub struct StringCache(HashMap); @@ -13,16 +12,14 @@ pub struct StringCache(HashMap); impl StringCache { pub fn populate(data: &[u8], offsets: &[ChunkOffset]) -> DeserializationResult { let mut cache = HashMap::new(); - let mut cursor = Cursor::new(data); - let cursor_ref = cursor.borrow_mut(); for &offset in offsets.iter().filter(|&&offset| offset > 0) { - try_seek!(cursor_ref, offset, "first xml string")?; + let mut cursor = ByteCursor::with_pos(data, offset as usize)?; loop { - let string_position = cursor_ref.position() as ChunkOffset; - let link = BinXmlNameLink::from_stream(cursor_ref)?; - let name = BinXmlName::from_stream(cursor_ref)?; + let string_position = cursor.pos() as ChunkOffset; + let link = BinXmlNameLink::from_cursor(&mut cursor)?; + let name = BinXmlName::from_cursor(&mut cursor)?; cache.insert(string_position, name); @@ -33,7 +30,7 @@ impl StringCache { if offset == string_position { break; } - try_seek!(cursor_ref, offset, "next xml string")?; + cursor.set_pos(offset as usize, "next xml string")?; } None => break, } diff --git a/src/template_cache.rs b/src/template_cache.rs index 01e319c4..330e0ba2 100644 --- a/src/template_cache.rs +++ b/src/template_cache.rs @@ -1,14 +1,13 @@ -use crate::binxml::tokens::read_template_definition; +use crate::binxml::tokens::read_template_definition_cursor; use crate::err::DeserializationResult; use crate::ChunkOffset; use crate::model::deserialized::BinXMLTemplateDefinition; +use crate::utils::ByteCursor; use encoding::EncodingRef; use log::trace; -use std::borrow::BorrowMut; use std::collections::HashMap; -use std::io::{Cursor, Seek, SeekFrom}; pub type CachedTemplate<'chunk> = BinXMLTemplateDefinition<'chunk>; @@ -26,15 +25,13 @@ impl<'chunk> TemplateCache<'chunk> { ansi_codec: EncodingRef, ) -> DeserializationResult { let mut cache = HashMap::new(); - let mut cursor = Cursor::new(data); - let cursor_ref = cursor.borrow_mut(); for offset in offsets.iter().filter(|&&offset| offset > 0) { - try_seek!(cursor_ref, offset, "first template")?; + let mut cursor = ByteCursor::with_pos(data, *offset as usize)?; loop { - let table_offset = cursor_ref.position() as ChunkOffset; - let definition = read_template_definition(cursor_ref, None, ansi_codec)?; + let table_offset = cursor.pos() as ChunkOffset; + let definition = read_template_definition_cursor(&mut cursor, None, ansi_codec)?; let next_template_offset = definition.header.next_template_offset; cache.insert(table_offset, definition); @@ -45,7 +42,7 @@ impl<'chunk> TemplateCache<'chunk> { break; } - try_seek!(cursor_ref, next_template_offset, "next template")?; + cursor.set_pos(next_template_offset as usize, "next template")?; } } diff --git a/src/utils/binxml_utils.rs b/src/utils/binxml_utils.rs deleted file mode 100644 index ef2c8c08..00000000 --- a/src/utils/binxml_utils.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::evtx_parser::ReadSeek; -use thiserror::Error; - -use crate::err::{DeserializationError, DeserializationResult, WrappedIoError}; - -use byteorder::{LittleEndian, ReadBytesExt}; - -use encoding::{DecoderTrap, EncodingRef, decode}; -use log::trace; -use std::char::decode_utf16; -use std::error::Error as StdErr; -use std::io::{self, Error, ErrorKind}; - -#[derive(Debug, Error)] -pub enum FailedToReadString { - #[error("An I/O error has occurred")] - IoError(#[from] io::Error), -} - -pub fn read_len_prefixed_utf16_string( - stream: &mut T, - is_null_terminated: bool, -) -> Result, FailedToReadString> { - let expected_number_of_characters = stream.read_u16::()?; - let needed_bytes = u64::from(expected_number_of_characters * 2); - - trace!( - "Offset `0x{offset:08x} ({offset})` reading a{nul}string of len {len}", - offset = stream.tell().unwrap_or(0), - nul = if is_null_terminated { - " null terminated " - } else { - " " - }, - len = expected_number_of_characters - ); - - let s = read_utf16_by_size(stream, needed_bytes)?; - - if is_null_terminated { - stream.read_u16::()?; - }; - - // It is useless to check for size equality, since u16 characters may be decoded into multiple u8 chars, - // so we might end up with more characters than originally asked for. - // - // Moreover, the code will also not read **less** characters than asked. - Ok(s) -} - -/// Reads a utf16 string from the given stream. -/// size is the actual byte representation of the string (not the number of characters). -pub fn read_utf16_by_size(stream: &mut T, size: u64) -> io::Result> { - match size { - 0 => Ok(None), - _ => read_utf16_string(stream, Some(size as usize / 2)).map(|mut s| { - // Efficiently remove trailing whitespace (for example, spaces) by modifying in place. - let trimmed_len = s.trim_end().len(); - if trimmed_len < s.len() { - s.truncate(trimmed_len); - } - Some(s) - }), - } -} -/// Reads an ansi encoded string from the given stream using `ansi_codec`. -pub fn read_ansi_encoded_string( - stream: &mut T, - size: u64, - ansi_codec: EncodingRef, -) -> DeserializationResult> { - match size { - 0 => Ok(None), - _ => { - let mut bytes = vec![0; size as usize]; - stream.read_exact(&mut bytes)?; - - // There may be multiple NULs in the string, prune them. - bytes.retain(|&b| b != 0); - - let s = match decode(&bytes, DecoderTrap::Strict, ansi_codec).0 { - Ok(s) => s, - Err(message) => { - let as_boxed_err = Box::::from(message.to_string()); - let wrapped_io_err = WrappedIoError::capture_hexdump(as_boxed_err, stream); - return Err(DeserializationError::FailedToReadToken { - t: format!("ansi_string {}", ansi_codec.name()), - token_name: "", - source: wrapped_io_err, - }); - } - }; - - Ok(Some(s)) - } - } -} - -pub fn read_null_terminated_utf16_string(stream: &mut T) -> io::Result { - read_utf16_string(stream, None) -} - -/// Reads a utf16 string from the given stream. -/// If `len` is given, exactly `len` u16 values are read from the stream. -/// If `len` is None, the string is assumed to be null terminated and the stream will be read to the first null (0). -fn read_utf16_string(stream: &mut T, len: Option) -> io::Result { - let mut buffer = match len { - Some(len) => Vec::with_capacity(len), - None => Vec::new(), - }; - - match len { - Some(len) => { - for _ in 0..len { - let next_char = stream.read_u16::()?; - buffer.push(next_char); - } - } - None => loop { - let next_char = stream.read_u16::()?; - - if next_char == 0 { - break; - } - - buffer.push(next_char); - }, - } - - // We need to stop if we see a NUL byte, even if asked for more bytes. - decode_utf16(buffer.into_iter().take_while(|&byte| byte != 0x00)) - .map(|r| r.map_err(|_e| Error::from(ErrorKind::InvalidData))) - .collect() -} diff --git a/src/utils/byte_cursor.rs b/src/utils/byte_cursor.rs new file mode 100644 index 00000000..8cd0e4e6 --- /dev/null +++ b/src/utils/byte_cursor.rs @@ -0,0 +1,314 @@ +use crate::err::{DeserializationError, DeserializationResult}; +use crate::utils::bytes; +use std::io; + +/// A lightweight cursor over an immutable byte slice. +/// +/// This is the slice/offset equivalent of `Cursor<&[u8]>`, intended for hot-path parsing where: +/// - the data is already in memory, and +/// - we want explicit bounds/offset control without IO-style error plumbing. +/// +/// All reads are little-endian and advance the cursor on success. +#[derive(Clone, Copy, Debug)] +pub(crate) struct ByteCursor<'a> { + buf: &'a [u8], + pos: usize, +} + +impl<'a> ByteCursor<'a> { + #[inline] + pub(crate) fn with_pos(buf: &'a [u8], pos: usize) -> DeserializationResult { + // Allow pos == len (EOF), reject pos > len. + let _ = bytes::slice_r(buf, pos, 0, "cursor.position")?; + Ok(Self { buf, pos }) + } + + #[inline] + pub(crate) fn buf(&self) -> &'a [u8] { + self.buf + } + + #[inline] + pub(crate) fn pos(&self) -> usize { + self.pos + } + + #[inline] + pub(crate) fn position(&self) -> u64 { + self.pos as u64 + } + + #[inline] + pub(crate) fn set_pos(&mut self, pos: usize, what: &'static str) -> DeserializationResult<()> { + let _ = bytes::slice_r(self.buf, pos, 0, what)?; + self.pos = pos; + Ok(()) + } + + #[inline] + pub(crate) fn set_pos_u64( + &mut self, + pos: u64, + what: &'static str, + ) -> DeserializationResult<()> { + let pos_usize = usize::try_from(pos).map_err(|_| DeserializationError::Truncated { + what, + offset: pos, + need: 0, + have: 0, + })?; + self.set_pos(pos_usize, what) + } + + #[inline] + pub(crate) fn advance(&mut self, n: usize, what: &'static str) -> DeserializationResult<()> { + let new_pos = self + .pos + .checked_add(n) + .ok_or(DeserializationError::Truncated { + what, + offset: self.pos as u64, + need: n, + have: self.buf.len().saturating_sub(self.pos), + })?; + self.set_pos(new_pos, what) + } + + #[inline] + pub(crate) fn take_bytes( + &mut self, + len: usize, + what: &'static str, + ) -> DeserializationResult<&'a [u8]> { + let out = bytes::slice_r(self.buf, self.pos, len, what)?; + self.pos += len; + Ok(out) + } + + #[inline] + pub(crate) fn array( + &mut self, + what: &'static str, + ) -> DeserializationResult<[u8; N]> { + let v = bytes::read_array_r::(self.buf, self.pos, what)?; + self.pos += N; + Ok(v) + } + + #[inline] + pub(crate) fn u8(&mut self) -> DeserializationResult { + self.u8_named("u8") + } + + #[inline] + pub(crate) fn u8_named(&mut self, what: &'static str) -> DeserializationResult { + let b = bytes::read_u8(self.buf, self.pos).ok_or(DeserializationError::Truncated { + what, + offset: self.pos as u64, + need: 1, + have: self.buf.len().saturating_sub(self.pos), + })?; + self.pos += 1; + Ok(b) + } + + #[inline] + pub(crate) fn u16(&mut self) -> DeserializationResult { + self.u16_named("u16") + } + + #[inline] + pub(crate) fn u16_named(&mut self, what: &'static str) -> DeserializationResult { + let v = bytes::read_u16_le_r(self.buf, self.pos, what)?; + self.pos += 2; + Ok(v) + } + + #[inline] + pub(crate) fn u32(&mut self) -> DeserializationResult { + self.u32_named("u32") + } + + #[inline] + pub(crate) fn u32_named(&mut self, what: &'static str) -> DeserializationResult { + let v = bytes::read_u32_le_r(self.buf, self.pos, what)?; + self.pos += 4; + Ok(v) + } + + #[inline] + pub(crate) fn u64(&mut self) -> DeserializationResult { + self.u64_named("u64") + } + + #[inline] + pub(crate) fn u64_named(&mut self, what: &'static str) -> DeserializationResult { + let v = bytes::read_u64_le_r(self.buf, self.pos, what)?; + self.pos += 8; + Ok(v) + } + + /// Read a sized array encoded as "N bytes of consecutive elements". + /// + /// This matches the historical behavior of the old `try_read_sized_array` helpers: + /// we stop when we've *consumed at least* `size_bytes` bytes since the start of this call. + /// + /// `elem_bytes` is only used for capacity preallocation. + pub(crate) fn read_sized_vec( + &mut self, + size_bytes: u16, + elem_bytes: usize, + mut read_one: impl FnMut(&mut Self) -> DeserializationResult, + ) -> DeserializationResult> { + let size_usize = usize::from(size_bytes); + if size_usize == 0 { + return Ok(Vec::new()); + } + + let start = self.pos; + let mut out = Vec::with_capacity(size_usize / elem_bytes.max(1)); + loop { + let cur = self.pos; + if (cur - start) >= size_usize { + break; + } + out.push(read_one(self)?); + } + Ok(out) + } + + /// Read a sized array encoded as `size_bytes` bytes of consecutive **fixed-width** elements, + /// with strict alignment validation. + /// + /// - Validates `size_bytes % ELEM_BYTES == 0` + /// - Reads *exactly* `size_bytes / ELEM_BYTES` elements + /// - Uses a single bounds check (`take_bytes`) and then parses by iterating `chunks_exact` + /// + /// The parse closure also receives the **absolute byte offset** (within this cursor’s backing + /// slice) of the current element, which is useful for precise error reporting. + pub(crate) fn read_sized_vec_aligned( + &mut self, + size_bytes: u16, + what: &'static str, + mut parse_one: impl FnMut(u64, &[u8; ELEM_BYTES]) -> DeserializationResult, + ) -> DeserializationResult> { + let size_usize = usize::from(size_bytes); + if size_usize == 0 { + return Ok(Vec::new()); + } + if ELEM_BYTES == 0 { + return Err(DeserializationError::Truncated { + what, + offset: self.pos as u64, + need: size_usize, + have: self.buf.len().saturating_sub(self.pos), + }); + } + if (size_usize % ELEM_BYTES) != 0 { + return Err(DeserializationError::Io(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "{what}: misaligned sized array (size_bytes={size_usize}, elem_bytes={ELEM_BYTES}) at offset {}", + self.pos + ), + ))); + } + + let start_pos = self.pos; + let bytes = self.take_bytes(size_usize, what)?; + let count = size_usize / ELEM_BYTES; + let mut out = Vec::with_capacity(count); + for (i, chunk) in bytes.chunks_exact(ELEM_BYTES).enumerate() { + let off = start_pos + i * ELEM_BYTES; + let arr: &[u8; ELEM_BYTES] = chunk + .try_into() + .expect("chunks_exact yields slices of the requested size"); + out.push(parse_one(off as u64, arr)?); + } + Ok(out) + } + + #[inline] + fn invalid_data(what: &'static str, offset: u64) -> DeserializationError { + DeserializationError::Io(io::Error::new( + io::ErrorKind::InvalidData, + format!("{what} at offset {offset}: invalid data"), + )) + } + + /// Read `char_count` UTF-16 code units (little-endian), decode (stop at NUL if present), + /// and trim trailing whitespace. + pub(crate) fn utf16_by_char_count_trimmed( + &mut self, + char_count: usize, + what: &'static str, + ) -> DeserializationResult> { + if char_count == 0 { + return Ok(None); + } + + let byte_len = char_count + .checked_mul(2) + .ok_or(DeserializationError::Truncated { + what, + offset: self.pos as u64, + need: usize::MAX, + have: self.buf.len().saturating_sub(self.pos), + })?; + + let bytes = self.take_bytes(byte_len, what)?; + if !bytes.len().is_multiple_of(2) { + return Err(Self::invalid_data(what, self.pos as u64)); + } + + let mut units = Vec::with_capacity(bytes.len() / 2); + for chunk in bytes.chunks_exact(2) { + units.push(u16::from_le_bytes([chunk[0], chunk[1]])); + } + + let mut s = crate::utils::decode_utf16_units_z(&units) + .map_err(|_| Self::invalid_data(what, (self.pos - byte_len) as u64))?; + + // Match historical behavior: trim trailing whitespace (common in EVTX strings). + let trimmed_len = s.trim_end().len(); + if trimmed_len < s.len() { + s.truncate(trimmed_len); + } + + Ok(Some(s)) + } + + /// Read a `u16` length prefix (number of UTF-16 code units), then that many code units, + /// decoding until NUL (if present). Optionally reads and discards a trailing NUL code unit. + pub(crate) fn len_prefixed_utf16_string( + &mut self, + is_null_terminated: bool, + what: &'static str, + ) -> DeserializationResult> { + let char_count = self.u16_named(what)? as usize; + let s = self.utf16_by_char_count_trimmed(char_count, what)?; + if is_null_terminated { + let _ = self.u16_named(what)?; + } + Ok(s) + } + + /// Read UTF-16 code units until a NUL (0x0000) code unit is encountered. + pub(crate) fn null_terminated_utf16_string( + &mut self, + what: &'static str, + ) -> DeserializationResult { + let start = self.pos; + let mut units: Vec = Vec::new(); + loop { + let cu = self.u16_named(what)?; + if cu == 0 { + break; + } + units.push(cu); + } + + crate::utils::decode_utf16_units_z(&units) + .map_err(|_| Self::invalid_data(what, start as u64)) + } +} diff --git a/src/utils/bytes.rs b/src/utils/bytes.rs new file mode 100644 index 00000000..b59eeb71 --- /dev/null +++ b/src/utils/bytes.rs @@ -0,0 +1,143 @@ +//! Byte-slice utilities for bounds-oriented parsing. +//! +//! This module is intentionally tiny and *boring*: it provides a consistent, well-documented way +//! to read little-endian primitives out of `&[u8]` at fixed offsets, with minimal overhead. +//! +//! There are two layers: +//! - **Option layer** (`read_*`): zero-cost helpers that return `Option`. +//! Use these when you want to map failures to your own error type (e.g. WEVT parsing). +//! - **Result layer** (`*_r`): wrappers that map `None` to `DeserializationError::Truncated`. +//! Use these for EVTX parsing where `DeserializationError` is the canonical error type. +//! +//! Design notes: +//! - All numeric reads are **little-endian** (EVTX/WEVT data is LE). +//! - Offsets are `usize` and are interpreted relative to the slice you pass in. +//! - Prefer a single up-front bounds check with [`slice_r`] when parsing fixed-size structs. +//! +//! Example (fixed-size header parsing): +//! +//! ```ignore +//! use crate::utils::bytes; +//! +//! // Ensure the struct is present, then read fields by fixed offsets. +//! let _ = bytes::slice_r(buf, 0, 128, "EVTX file header")?; +//! let magic = bytes::read_array_r::<8>(buf, 0, "file header magic")?; +//! let flags = bytes::read_u32_le_r(buf, 120, "file header flags")?; +//! ``` + +use crate::err::DeserializationError; + +/// Read `N` raw bytes at `offset`. +/// +/// Returns `None` if the range is out of bounds. +pub(crate) fn read_array(buf: &[u8], offset: usize) -> Option<[u8; N]> { + let end = offset.checked_add(N)?; + let bytes: [u8; N] = buf.get(offset..end)?.try_into().ok()?; + Some(bytes) +} + +/// Read a single byte at `offset`. +pub(crate) fn read_u8(buf: &[u8], offset: usize) -> Option { + buf.get(offset).copied() +} + +/// Read a 4-byte signature at `offset` (e.g. `b\"ElfChnk\\0\"[..4]` style). +pub(crate) fn read_sig(buf: &[u8], offset: usize) -> Option<[u8; 4]> { + read_array::<4>(buf, offset) +} + +/// Read a `u16` (little-endian) at `offset`. +pub(crate) fn read_u16_le(buf: &[u8], offset: usize) -> Option { + Some(u16::from_le_bytes(read_array::<2>(buf, offset)?)) +} + +/// Read a `u32` (little-endian) at `offset`. +pub(crate) fn read_u32_le(buf: &[u8], offset: usize) -> Option { + Some(u32::from_le_bytes(read_array::<4>(buf, offset)?)) +} + +/// Read a `u64` (little-endian) at `offset`. +pub(crate) fn read_u64_le(buf: &[u8], offset: usize) -> Option { + Some(u64::from_le_bytes(read_array::<8>(buf, offset)?)) +} + +#[inline] +fn truncated(what: &'static str, offset: usize, need: usize, len: usize) -> DeserializationError { + DeserializationError::Truncated { + what, + offset: offset as u64, + need, + have: len.saturating_sub(offset), + } +} + +pub(crate) fn slice_r<'a>( + buf: &'a [u8], + offset: usize, + len: usize, + what: &'static str, +) -> Result<&'a [u8], DeserializationError> { + let end = offset + .checked_add(len) + .ok_or_else(|| truncated(what, offset, len, buf.len()))?; + buf.get(offset..end) + .ok_or_else(|| truncated(what, offset, len, buf.len())) +} + +/// Read `N` raw bytes at `offset`, or return `DeserializationError::Truncated`. +pub(crate) fn read_array_r( + buf: &[u8], + offset: usize, + what: &'static str, +) -> Result<[u8; N], DeserializationError> { + read_array::(buf, offset).ok_or_else(|| truncated(what, offset, N, buf.len())) +} + +/// Read a `u16` (little-endian) at `offset`, or return `DeserializationError::Truncated`. +pub(crate) fn read_u16_le_r( + buf: &[u8], + offset: usize, + what: &'static str, +) -> Result { + read_u16_le(buf, offset).ok_or_else(|| truncated(what, offset, 2, buf.len())) +} + +/// Read a `u32` (little-endian) at `offset`, or return `DeserializationError::Truncated`. +pub(crate) fn read_u32_le_r( + buf: &[u8], + offset: usize, + what: &'static str, +) -> Result { + read_u32_le(buf, offset).ok_or_else(|| truncated(what, offset, 4, buf.len())) +} + +/// Read a `u64` (little-endian) at `offset`, or return `DeserializationError::Truncated`. +pub(crate) fn read_u64_le_r( + buf: &[u8], + offset: usize, + what: &'static str, +) -> Result { + read_u64_le(buf, offset).ok_or_else(|| truncated(what, offset, 8, buf.len())) +} + +/// Read a `count`-element `u32` (little-endian) table at `offset`. +/// +/// This does a single bounds check for the whole table and then reads each element. +pub(crate) fn read_u32_vec_le_r( + buf: &[u8], + offset: usize, + count: usize, + what: &'static str, +) -> Result, DeserializationError> { + // Fast fail on table bounds, then read each entry without re-checking offset math overflow. + let bytes = count + .checked_mul(4) + .ok_or_else(|| truncated(what, offset, usize::MAX, buf.len()))?; + let _ = slice_r(buf, offset, bytes, what)?; + + let mut out = Vec::with_capacity(count); + for i in 0..count { + out.push(read_u32_le_r(buf, offset + i * 4, what)?); + } + Ok(out) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 112e79cb..382f1624 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,10 +1,11 @@ -mod binxml_utils; +pub(crate) mod byte_cursor; +pub(crate) mod bytes; pub(super) mod hexdump; -mod time; +mod parse_error; +pub(crate) mod utf16; +pub(crate) mod windows; -pub use self::binxml_utils::{ - read_ansi_encoded_string, read_len_prefixed_utf16_string, read_null_terminated_utf16_string, - read_utf16_by_size, -}; +pub(crate) use self::byte_cursor::ByteCursor; pub use self::hexdump::dump_stream; -pub use self::time::read_systemtime; +pub(crate) use self::parse_error::invalid_data; +pub(crate) use self::utf16::{decode_utf16_units_z, decode_utf16le_bytes_z}; diff --git a/src/utils/parse_error.rs b/src/utils/parse_error.rs new file mode 100644 index 00000000..27350b13 --- /dev/null +++ b/src/utils/parse_error.rs @@ -0,0 +1,11 @@ +use std::io; + +use crate::err::DeserializationError; + +#[inline] +pub(crate) fn invalid_data(what: &'static str, offset: u64) -> DeserializationError { + DeserializationError::Io(io::Error::new( + io::ErrorKind::InvalidData, + format!("{what} at offset {offset}: invalid data"), + )) +} diff --git a/src/utils/time.rs b/src/utils/time.rs deleted file mode 100644 index df88be2c..00000000 --- a/src/utils/time.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::err::{DeserializationError, DeserializationResult}; - -use crate::evtx_parser::ReadSeek; -use byteorder::ReadBytesExt; -use chrono::prelude::*; - -pub fn read_systemtime(r: &mut R) -> DeserializationResult> { - let year = i32::from(try_read!(r, u16)?); - let month = u32::from(try_read!(r, u16)?); - let _day_of_week = try_read!(r, u16)?; - let day = u32::from(try_read!(r, u16)?); - let hour = u32::from(try_read!(r, u16)?); - let minute = u32::from(try_read!(r, u16)?); - let second = u32::from(try_read!(r, u16)?); - let milliseconds = u32::from(try_read!(r, u16)?); - - // The entire value is unset. By convention, use the "1601-01-01T00:00:00.0000000Z" timestamp. - if year == 0 - && month == 0 - && day == 0 - && hour == 0 - && minute == 0 - && second == 0 - && milliseconds == 0 - { - return Ok(Utc.from_utc_datetime( - &NaiveDate::from_ymd_opt(1601, 1, 1) - .expect("Always valid") - .and_hms_nano_opt(0, 0, 0, 0) - .expect("Always valid"), - )); - } - - Ok(Utc.from_utc_datetime( - &NaiveDate::from_ymd_opt(year, month, day) - .ok_or(DeserializationError::InvalidDateTimeError)? - .and_hms_nano_opt(hour, minute, second, milliseconds * 1_000_000) // Convert milliseconds to nanoseconds - .ok_or(DeserializationError::InvalidDateTimeError)?, - )) -} - -#[cfg(test)] -mod tests { - use std::io::Cursor; - - use chrono::{Datelike, NaiveDate, TimeZone, Utc}; - - use super::read_systemtime; - - #[test] - fn test_date_regular() { - let data = [227u8, 7, 3, 0, 5, 0, 8, 0, 23, 0, 22, 0, 5, 0, 0, 0]; - - let date = read_systemtime(&mut Cursor::new(data)).unwrap(); - let expected_date = Utc.from_utc_datetime( - &NaiveDate::from_ymd_opt(2019, 3, 8) - .unwrap() - .and_hms_nano_opt(23, 22, 5, 0) - .unwrap(), - ); - assert_eq!(date, expected_date); - } - - #[test] - fn test_date_invalid_month() { - // No such month. - let data = [227u8, 7, 255, 0, 5, 0, 8, 0, 23, 0, 22, 0, 5, 0, 0, 0]; - let date_res = read_systemtime(&mut Cursor::new(data)); - assert!(date_res.is_err()); - } - - #[test] - fn test_date_invalid_time() { - // No such hour 255. - let data = [227u8, 7, 3, 0, 5, 0, 8, 0, 255, 0, 22, 0, 5, 0, 0, 0]; - let date_res = read_systemtime(&mut Cursor::new(data)); - assert!(date_res.is_err()); - } - - #[test] - fn test_date_zero() { - let data = [0u8; 16]; - let date = read_systemtime(&mut Cursor::new(data)).unwrap(); - assert_eq!(date.year_ce(), (true, 1601)); - } -} diff --git a/src/utils/utf16.rs b/src/utils/utf16.rs new file mode 100644 index 00000000..9c40aa68 --- /dev/null +++ b/src/utils/utf16.rs @@ -0,0 +1,27 @@ +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) enum Utf16LeDecodeError { + OddLength, + InvalidData, +} + +/// Decode a UTF-16LE byte slice until the first NUL (0x0000), if present. +pub(crate) fn decode_utf16le_bytes_z(bytes: &[u8]) -> Result { + if !bytes.len().is_multiple_of(2) { + return Err(Utf16LeDecodeError::OddLength); + } + + let mut units = Vec::with_capacity(bytes.len() / 2); + for chunk in bytes.chunks_exact(2) { + units.push(u16::from_le_bytes([chunk[0], chunk[1]])); + } + + decode_utf16_units_z(&units) +} + +/// Decode UTF-16 code units until the first NUL (0x0000), if present. +pub(crate) fn decode_utf16_units_z(units: &[u16]) -> Result { + let end = units.iter().position(|&c| c == 0).unwrap_or(units.len()); + String::from_utf16(&units[..end]).map_err(|_| Utf16LeDecodeError::InvalidData) +} + + diff --git a/src/utils/windows.rs b/src/utils/windows.rs new file mode 100644 index 00000000..e9972be7 --- /dev/null +++ b/src/utils/windows.rs @@ -0,0 +1,80 @@ +use std::io; +use std::io::Cursor; + +use chrono::{DateTime, Duration, NaiveDate, TimeZone, Utc}; +use winstructs::security::Sid; + +use crate::err::{DeserializationError, DeserializationResult}; +use crate::utils::ByteCursor; + +#[inline] +pub(crate) fn filetime_to_datetime(filetime: u64) -> DateTime { + // Match historical behavior (`winstructs::timestamp::WinTimestamp::to_datetime`). + let naive = NaiveDate::from_ymd_opt(1601, 1, 1) + .and_then(|x| x.and_hms_nano_opt(0, 0, 0, 0)) + .expect("filetime epoch should be valid") + + Duration::microseconds((filetime / 10) as i64); + Utc.from_utc_datetime(&naive) +} + +pub(crate) fn read_systime(cursor: &mut ByteCursor<'_>) -> DeserializationResult> { + let bytes = cursor.array::<16>("systime")?; + systime_from_bytes(&bytes) +} + +pub(crate) fn systime_from_bytes(bytes: &[u8; 16]) -> DeserializationResult> { + let year = i32::from(u16::from_le_bytes([bytes[0], bytes[1]])); + let month = u32::from(u16::from_le_bytes([bytes[2], bytes[3]])); + let _day_of_week = u16::from_le_bytes([bytes[4], bytes[5]]); + let day = u32::from(u16::from_le_bytes([bytes[6], bytes[7]])); + let hour = u32::from(u16::from_le_bytes([bytes[8], bytes[9]])); + let minute = u32::from(u16::from_le_bytes([bytes[10], bytes[11]])); + let second = u32::from(u16::from_le_bytes([bytes[12], bytes[13]])); + let milliseconds = u32::from(u16::from_le_bytes([bytes[14], bytes[15]])); + + // The entire value is unset. By convention, use the "1601-01-01T00:00:00.0000000Z" timestamp. + if year == 0 + && month == 0 + && day == 0 + && hour == 0 + && minute == 0 + && second == 0 + && milliseconds == 0 + { + return Ok(Utc.from_utc_datetime( + &NaiveDate::from_ymd_opt(1601, 1, 1) + .expect("Always valid") + .and_hms_nano_opt(0, 0, 0, 0) + .expect("Always valid"), + )); + } + + Ok(Utc.from_utc_datetime( + &NaiveDate::from_ymd_opt(year, month, day) + .ok_or(DeserializationError::InvalidDateTimeError)? + .and_hms_nano_opt(hour, minute, second, milliseconds * 1_000_000) // Convert milliseconds to nanoseconds + .ok_or(DeserializationError::InvalidDateTimeError)?, + )) +} + +pub(crate) fn read_sid(cursor: &mut ByteCursor<'_>) -> DeserializationResult { + let start = cursor.pos(); + let remaining = cursor + .buf() + .get(start..) + .ok_or(DeserializationError::Truncated { + what: "sid", + offset: start as u64, + need: 1, + have: 0, + })?; + + let mut c = Cursor::new(remaining); + let sid = Sid::from_reader(&mut c).map_err(|e| { + DeserializationError::Io(io::Error::new(io::ErrorKind::InvalidData, e)) + })?; + cursor.advance(c.position() as usize, "sid")?; + Ok(sid) +} + + diff --git a/src/wevt_templates/binxml.rs b/src/wevt_templates/binxml.rs new file mode 100644 index 00000000..d101432e --- /dev/null +++ b/src/wevt_templates/binxml.rs @@ -0,0 +1,100 @@ +//! BinXML parsing helpers for the WEVT template “inline-name” dialect. +//! +//! Template BinXML (inside CRIM/TTBL/TEMP) does not use the normal EVTX string-table name +//! references. Instead, element/attribute names are stored inline (MS-EVEN6 Name structure). +//! We expose token streams so higher-level code can render skeletons or apply substitution +//! values without duplicating deserializer setup. +//! +//! References: +//! - `docs/wevt_templates.md` (project notes + curated links) +//! - MS-EVEN6 (BinXml `Name` structure and NameHash) + +use encoding::EncodingRef; + +pub(super) const TEMP_BINXML_OFFSET: usize = 40; + +/// Parse the BinXML fragment embedded inside a `TEMP` entry. +/// +/// `TEMP` wraps a BinXML fragment plus trailing item descriptors/names; callers often want +/// *just the BinXML* (as tokens) for offline rendering or inspection. +/// +/// Returns `(tokens, bytes_consumed)` where `bytes_consumed` is the number of bytes read from the +/// BinXML fragment (starting at offset 40 from the beginning of `TEMP`). +pub fn parse_temp_binxml_fragment<'a>( + temp_bytes: &'a [u8], + ansi_codec: EncodingRef, +) -> crate::err::Result<( + Vec>, + u32, +)> { + use crate::binxml::deserializer::BinXmlDeserializer; + use crate::binxml::name::BinXmlNameEncoding; + use crate::err::EvtxError; + + if temp_bytes.len() < TEMP_BINXML_OFFSET { + return Err(EvtxError::calculation_error(format!( + "TEMP too small to contain BinXML fragment header (len={}, need >= {})", + temp_bytes.len(), + TEMP_BINXML_OFFSET + ))); + } + + let binxml = &temp_bytes[TEMP_BINXML_OFFSET..]; + let de = BinXmlDeserializer::init_with_name_encoding( + binxml, + 0, + None, + true, + ansi_codec, + BinXmlNameEncoding::WevtInline, + ); + + let mut iterator = de.iter_tokens(None)?; + let mut tokens = vec![]; + for t in iterator.by_ref() { + tokens.push(t?); + } + + let bytes_consumed = u32::try_from(iterator.position()) + .map_err(|_| EvtxError::calculation_error("BinXML fragment too large".to_string()))?; + + Ok((tokens, bytes_consumed)) +} + +/// Parse a WEVT_TEMPLATE BinXML fragment (inline-name encoding). +/// +/// Some callers already have the BinXML slice (e.g. from a parsed `manifest::TemplateDefinition`) +/// and need to deserialize it using the inline-name rules. +/// +/// Returns `(tokens, bytes_consumed)` where `bytes_consumed` is the number of bytes read from `binxml`. +pub fn parse_wevt_binxml_fragment<'a>( + binxml: &'a [u8], + ansi_codec: EncodingRef, +) -> crate::err::Result<( + Vec>, + u32, +)> { + use crate::binxml::deserializer::BinXmlDeserializer; + use crate::binxml::name::BinXmlNameEncoding; + use crate::err::EvtxError; + + let de = BinXmlDeserializer::init_with_name_encoding( + binxml, + 0, + None, + true, + ansi_codec, + BinXmlNameEncoding::WevtInline, + ); + + let mut iterator = de.iter_tokens(None)?; + let mut tokens = vec![]; + for t in iterator.by_ref() { + tokens.push(t?); + } + + let bytes_consumed = u32::try_from(iterator.position()) + .map_err(|_| EvtxError::calculation_error("BinXML fragment too large".to_string()))?; + + Ok((tokens, bytes_consumed)) +} diff --git a/src/wevt_templates/cache.rs b/src/wevt_templates/cache.rs new file mode 100644 index 00000000..d1377caf --- /dev/null +++ b/src/wevt_templates/cache.rs @@ -0,0 +1,349 @@ +#![allow(clippy::result_large_err)] + +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +use encoding::EncodingRef; +use serde_json::Value as JsonValue; +use thiserror::Error; + +use super::manifest::WevtManifestError; + +#[derive(Debug, Error)] +pub enum WevtCacheError { + #[error("failed to read WEVT cache index `{path}`: {source}")] + ReadIndex { path: PathBuf, source: io::Error }, + + #[error("invalid JSONL at {path}:{line_no}: {source}")] + InvalidJsonLine { + path: PathBuf, + line_no: usize, + source: serde_json::Error, + }, + + #[error("failed to read cache blob `{path}`: {source}")] + ReadBlob { path: PathBuf, source: io::Error }, + + #[error( + "TEMP slice out of bounds for `{path}` (offset={temp_offset}, size={temp_size}, len={len})" + )] + TempSliceOutOfBounds { + path: PathBuf, + temp_offset: u32, + temp_size: u32, + len: usize, + }, + + #[error("failed to parse CRIM/WEVT blob `{path}` while scanning templates: {source}")] + CrimParse { + path: PathBuf, + source: WevtManifestError, + }, + + #[error( + "template GUID `{guid}` not found in cache index `{index_path}` (and not discovered in any CRIM blobs)" + )] + TemplateNotFound { guid: String, index_path: PathBuf }, + + #[error("failed to render TEMP for template_guid={guid}: {source}")] + RenderTemp { + guid: String, + source: crate::err::EvtxError, + }, +} + +#[derive(Debug, Clone)] +struct TempBytes { + bytes: Arc>, + start: usize, + end: usize, +} + +impl TempBytes { + fn as_slice(&self) -> &[u8] { + &self.bytes[self.start..self.end] + } +} + +#[derive(Debug, Clone)] +enum TemplateSource { + /// A standalone TEMP blob written by `extract-wevt-templates --split-ttbl`. + TempFile(PathBuf), + /// A TEMP slice located inside a CRIM/WEVT blob (offset/size refer to the blob bytes). + CrimSlice { + path: PathBuf, + temp_offset: u32, + temp_size: u32, + }, +} + +/// Offline cache for extracted `WEVT_TEMPLATE` templates, keyed by template GUID. +/// +/// This is primarily intended for "offline rendering" workflows: +/// - Extract WEVT templates from provider binaries into a cache directory + JSONL index. +/// - Use this cache to render EVTX records when their embedded template definitions are missing or +/// fail to deserialize. +/// +/// The cache index format is produced by the `evtx_dump extract-wevt-templates` subcommand. +#[derive(Debug)] +pub struct WevtCache { + index_path: PathBuf, + crim_paths: Vec, + + sources_by_guid: Mutex>, + scanned_crims: Mutex>, + blob_cache: Mutex>>>, +} + +impl WevtCache { + /// Load a cache index JSONL produced by `evtx_dump extract-wevt-templates`. + pub fn load(index_path: impl AsRef) -> Result { + let index_path = index_path.as_ref().to_path_buf(); + let text = fs::read_to_string(&index_path).map_err(|source| WevtCacheError::ReadIndex { + path: index_path.clone(), + source, + })?; + + let mut crim_paths: Vec = Vec::new(); + let mut sources_by_guid: HashMap = HashMap::new(); + + fn resolve_output_path(index_path: &Path, output_path: &str) -> PathBuf { + let p = Path::new(output_path); + if p.is_absolute() { + return p.to_path_buf(); + } + let base = index_path.parent().unwrap_or_else(|| Path::new(".")); + base.join(p) + } + + for (line_no, line) in text.lines().enumerate() { + let line = line.trim(); + if line.is_empty() { + continue; + } + + let v: JsonValue = + serde_json::from_str(line).map_err(|source| WevtCacheError::InvalidJsonLine { + path: index_path.clone(), + line_no: line_no + 1, + source, + })?; + + // ExtractWevtTemplatesOutputLine: has output_path + size, but no guid/provider_guid/template_guid. + if v.get("output_path").and_then(|p| p.as_str()).is_some() + && v.get("size").is_some() + && v.get("guid").is_none() + && v.get("provider_guid").is_none() + && v.get("template_guid").is_none() + { + if let Some(p) = v.get("output_path").and_then(|p| p.as_str()) { + crim_paths.push(resolve_output_path(&index_path, p)); + } + continue; + } + + // ExtractWevtTempOutputLine: has guid + temp_offset/temp_size + output_path to a standalone TEMP blob. + if let (Some(guid), Some(output_path)) = ( + v.get("guid").and_then(|v| v.as_str()), + v.get("output_path").and_then(|v| v.as_str()), + ) { + // Only accept this as a TEMP binary when the expected TEMP fields exist. + if v.get("temp_offset").is_some() && v.get("temp_size").is_some() { + sources_by_guid.insert( + normalize_guid(guid), + TemplateSource::TempFile(resolve_output_path(&index_path, output_path)), + ); + } + } + } + + // De-dupe CRIM paths (index can contain repeats). + crim_paths.sort(); + crim_paths.dedup(); + + Ok(WevtCache { + index_path, + crim_paths, + sources_by_guid: Mutex::new(sources_by_guid), + scanned_crims: Mutex::new(HashSet::new()), + blob_cache: Mutex::new(HashMap::new()), + }) + } + + /// Render a template by GUID using the default Windows-1252 ANSI codec. + pub fn render_by_template_guid( + &self, + template_guid: &str, + substitutions: &[String], + ) -> Result { + self.render_by_template_guid_with_ansi_codec( + template_guid, + substitutions, + encoding::all::WINDOWS_1252, + ) + } + + /// Render a template by GUID using an explicit ANSI codec. + pub fn render_by_template_guid_with_ansi_codec( + &self, + template_guid: &str, + substitutions: &[String], + ansi_codec: EncodingRef, + ) -> Result { + let guid = normalize_guid(template_guid); + let temp_bytes = self.get_temp_bytes_for_guid(&guid)?; + + crate::wevt_templates::render_temp_to_xml_with_substitution_values( + temp_bytes.as_slice(), + substitutions, + ansi_codec, + ) + .map_err(|source| WevtCacheError::RenderTemp { guid, source }) + } + + fn get_temp_bytes_for_guid(&self, guid: &str) -> Result { + // Fast path: do we already know a source for this guid? + if let Some(tb) = self.try_load_from_known_source(guid)? { + return Ok(tb); + } + + // Otherwise, scan CRIM blobs until we discover the guid. + for crim_path in &self.crim_paths { + if self.is_scanned(crim_path) { + continue; + } + self.scan_crim_for_templates(crim_path)?; + + if let Some(tb) = self.try_load_from_known_source(guid)? { + return Ok(tb); + } + } + + Err(WevtCacheError::TemplateNotFound { + guid: guid.to_string(), + index_path: self.index_path.clone(), + }) + } + + fn is_scanned(&self, path: &Path) -> bool { + self.scanned_crims + .lock() + .expect("lock poisoned") + .contains(path) + } + + fn mark_scanned(&self, path: &Path) { + self.scanned_crims + .lock() + .expect("lock poisoned") + .insert(path.to_path_buf()); + } + + fn load_blob(&self, path: &Path) -> Result>, WevtCacheError> { + if let Some(existing) = self + .blob_cache + .lock() + .expect("lock poisoned") + .get(path) + .cloned() + { + return Ok(existing); + } + + let bytes = fs::read(path).map_err(|source| WevtCacheError::ReadBlob { + path: path.to_path_buf(), + source, + })?; + let bytes = Arc::new(bytes); + + self.blob_cache + .lock() + .expect("lock poisoned") + .insert(path.to_path_buf(), bytes.clone()); + + Ok(bytes) + } + + fn try_load_from_known_source(&self, guid: &str) -> Result, WevtCacheError> { + let src = { + self.sources_by_guid + .lock() + .expect("lock poisoned") + .get(guid) + .cloned() + }; + + let Some(src) = src else { + return Ok(None); + }; + + match src { + TemplateSource::TempFile(path) => { + let bytes = self.load_blob(&path)?; + Ok(Some(TempBytes { + bytes: bytes.clone(), + start: 0, + end: bytes.len(), + })) + } + TemplateSource::CrimSlice { + path, + temp_offset, + temp_size, + } => { + let bytes = self.load_blob(&path)?; + let start = temp_offset as usize; + let end = start.saturating_add(temp_size as usize); + if end > bytes.len() { + return Err(WevtCacheError::TempSliceOutOfBounds { + path, + temp_offset, + temp_size, + len: bytes.len(), + }); + } + Ok(Some(TempBytes { bytes, start, end })) + } + } + } + + fn scan_crim_for_templates(&self, crim_path: &Path) -> Result<(), WevtCacheError> { + let bytes = self.load_blob(crim_path)?; + + let templates = + match crate::wevt_templates::extract_temp_templates_from_wevt_blob(bytes.as_slice()) { + Ok(t) => t, + Err(source) => { + // Mark scanned so we don't repeatedly try a broken blob. + self.mark_scanned(crim_path); + return Err(WevtCacheError::CrimParse { + path: crim_path.to_path_buf(), + source, + }); + } + }; + + let mut map = self.sources_by_guid.lock().expect("lock poisoned"); + for t in templates { + let g = normalize_guid(&t.header.guid.to_string()); + map.entry(g).or_insert(TemplateSource::CrimSlice { + path: crim_path.to_path_buf(), + temp_offset: t.temp_offset, + temp_size: t.temp_size, + }); + } + + self.mark_scanned(crim_path); + Ok(()) + } +} + +pub fn normalize_guid(s: &str) -> String { + s.trim() + .trim_start_matches('{') + .trim_end_matches('}') + .to_ascii_lowercase() +} diff --git a/src/wevt_templates/error.rs b/src/wevt_templates/error.rs new file mode 100644 index 00000000..3d1b7f5c --- /dev/null +++ b/src/wevt_templates/error.rs @@ -0,0 +1,21 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum WevtTemplateExtractError { + #[error("input is not a valid PE file: {message}")] + InvalidPe { message: &'static str }, + + #[error("malformed PE file: {message}")] + MalformedPe { message: &'static str }, + + #[error("failed to map RVA 0x{rva:08x} to a file offset")] + UnmappedRva { rva: u32 }, + + #[error("resource directory is malformed: {message}")] + MalformedResource { message: &'static str }, + + #[error("failed to decode UTF-16 resource name")] + InvalidResourceName, +} + + diff --git a/src/wevt_templates/extract.rs b/src/wevt_templates/extract.rs new file mode 100644 index 00000000..5f66ff5d --- /dev/null +++ b/src/wevt_templates/extract.rs @@ -0,0 +1,324 @@ +//! PE resource extraction for `WEVT_TEMPLATE` blobs (via `goblin`). +//! +//! Template definitions are shipped as PE resources (not inside EVTX files), so building an +//! *offline template cache* requires extracting those blobs without relying on Windows APIs. +//! We use `goblin` for PE + resource-directory parsing and keep only minimal glue code here. +//! +//! References: +//! - `docs/wevt_templates.md` (project notes + curated links) +//! - Microsoft PE/COFF specification (resource directory layout) + +use super::error::WevtTemplateExtractError; +use super::types::{ResourceIdentifier, WevtTemplateResource}; +use crate::utils::bytes; + +use goblin::pe::options::{ParseMode, ParseOptions}; +use goblin::pe::resource::{ImageResourceDirectory, ResourceDataEntry, ResourceEntry}; +use goblin::pe::section_table::SectionTable; + +const IMAGE_RESOURCE_DIRECTORY_HEADER_SIZE: usize = 16; +const RESOURCE_DATA_ENTRY_SIZE: usize = 16; + +fn rva_to_file_offset( + sections: &[SectionTable], + file_alignment: u32, + opts: &ParseOptions, + rva: u32, +) -> Option { + goblin::pe::utils::find_offset(rva as usize, sections, file_alignment, opts) +} + +fn parse_image_resource_directory( + rsrc: &[u8], + offset: usize, +) -> Result { + if offset + IMAGE_RESOURCE_DIRECTORY_HEADER_SIZE > rsrc.len() { + return Err(WevtTemplateExtractError::MalformedResource { + message: "resource directory header out of bounds", + }); + } + + Ok(ImageResourceDirectory { + characteristics: bytes::read_u32_le(rsrc, offset).ok_or( + WevtTemplateExtractError::MalformedResource { + message: "resource directory characteristics out of bounds", + }, + )?, + time_date_stamp: bytes::read_u32_le(rsrc, offset + 4).ok_or( + WevtTemplateExtractError::MalformedResource { + message: "resource directory time_date_stamp out of bounds", + }, + )?, + major_version: bytes::read_u16_le(rsrc, offset + 8).ok_or( + WevtTemplateExtractError::MalformedResource { + message: "resource directory major_version out of bounds", + }, + )?, + minor_version: bytes::read_u16_le(rsrc, offset + 10).ok_or( + WevtTemplateExtractError::MalformedResource { + message: "resource directory minor_version out of bounds", + }, + )?, + number_of_named_entries: bytes::read_u16_le(rsrc, offset + 12).ok_or( + WevtTemplateExtractError::MalformedResource { + message: "resource directory number_of_named_entries out of bounds", + }, + )?, + number_of_id_entries: bytes::read_u16_le(rsrc, offset + 14).ok_or( + WevtTemplateExtractError::MalformedResource { + message: "resource directory number_of_id_entries out of bounds", + }, + )?, + }) +} + +fn parse_resource_name(rsrc: &[u8], offset: usize) -> Result { + let char_count = + bytes::read_u16_le(rsrc, offset).ok_or(WevtTemplateExtractError::MalformedResource { + message: "resource name length out of bounds", + })? as usize; + + let bytes_off = offset + .checked_add(2) + .ok_or(WevtTemplateExtractError::MalformedResource { + message: "resource name offset overflow", + })?; + let bytes_len = + char_count + .checked_mul(2) + .ok_or(WevtTemplateExtractError::MalformedResource { + message: "resource name length overflow", + })?; + let bytes_end = + bytes_off + .checked_add(bytes_len) + .ok_or(WevtTemplateExtractError::MalformedResource { + message: "resource name end overflow", + })?; + + let buf = + rsrc.get(bytes_off..bytes_end) + .ok_or(WevtTemplateExtractError::MalformedResource { + message: "resource name out of bounds", + })?; + + let mut chars = Vec::with_capacity(char_count); + for i in 0..char_count { + let c = + bytes::read_u16_le(buf, i * 2).ok_or(WevtTemplateExtractError::MalformedResource { + message: "resource name read out of bounds", + })?; + chars.push(c); + } + + String::from_utf16(&chars).map_err(|_| WevtTemplateExtractError::InvalidResourceName) +} + +fn entry_identifier( + entry: ResourceEntry, + rsrc: &[u8], +) -> Result { + if entry.name_is_string() { + let name_offset = entry.name_offset() as usize; + Ok(ResourceIdentifier::Name(parse_resource_name( + rsrc, + name_offset, + )?)) + } else { + Ok(ResourceIdentifier::Id(entry.name_offset())) + } +} + +fn directory_entries( + rsrc: &[u8], + dir_offset: usize, +) -> Result, WevtTemplateExtractError> { + let dir = parse_image_resource_directory(rsrc, dir_offset)?; + let entries_offset = dir_offset + .checked_add(IMAGE_RESOURCE_DIRECTORY_HEADER_SIZE) + .ok_or(WevtTemplateExtractError::MalformedResource { + message: "resource directory entries offset overflow", + })?; + + let it = dir.next_iter(entries_offset, rsrc).map_err(|_| { + WevtTemplateExtractError::MalformedResource { + message: "resource directory entries out of bounds", + } + })?; + + it.collect::, _>>() + .map_err(|_| WevtTemplateExtractError::MalformedResource { + message: "failed to parse resource directory entries", + }) +} + +fn parse_resource_data_entry( + rsrc: &[u8], + offset: usize, +) -> Result { + if offset + RESOURCE_DATA_ENTRY_SIZE > rsrc.len() { + return Err(WevtTemplateExtractError::MalformedResource { + message: "resource data entry out of bounds", + }); + } + + Ok(ResourceDataEntry { + offset_to_data: bytes::read_u32_le(rsrc, offset).ok_or( + WevtTemplateExtractError::MalformedResource { + message: "resource data entry RVA out of bounds", + }, + )?, + size: bytes::read_u32_le(rsrc, offset + 4).ok_or( + WevtTemplateExtractError::MalformedResource { + message: "resource data entry size out of bounds", + }, + )?, + code_page: bytes::read_u32_le(rsrc, offset + 8).ok_or( + WevtTemplateExtractError::MalformedResource { + message: "resource data entry code_page out of bounds", + }, + )?, + reserved: bytes::read_u32_le(rsrc, offset + 12).ok_or( + WevtTemplateExtractError::MalformedResource { + message: "resource data entry reserved out of bounds", + }, + )?, + }) +} + +/// Extract `WEVT_TEMPLATE` resource blobs from a PE file. +/// +/// `WEVT_TEMPLATE` is where providers store the CRIM/WEVT template metadata needed to render +/// events offline (e.g. for an on-disk cache, or for CLI tooling like +/// `evtx_dump extract-wevt-templates`). This function lets us obtain those blobs cross-platform. +/// +/// Returns an empty vector if the PE has no resources or no `WEVT_TEMPLATE` resources. +pub fn extract_wevt_template_resources( + pe_bytes: &[u8], +) -> Result, WevtTemplateExtractError> { + let mut opts = ParseOptions::default(); + // We only need sections + data directories; parsing TLS/certificates is wasted work here. + opts.parse_tls_data = false; + opts.parse_attribute_certificates = false; + // Prefer permissive mode: we want template extraction to succeed even if unrelated tables + // (imports/debug/etc) are slightly malformed. + opts.parse_mode = ParseMode::Permissive; + + let pe = goblin::pe::PE::parse_with_opts(pe_bytes, &opts).map_err(|_| { + WevtTemplateExtractError::InvalidPe { + message: "failed to parse PE via goblin", + } + })?; + + let Some(optional_header) = pe.header.optional_header else { + return Err(WevtTemplateExtractError::InvalidPe { + message: "missing optional header", + }); + }; + + let Some(resource_table) = optional_header.data_directories.get_resource_table() else { + return Ok(Vec::new()); + }; + + if resource_table.virtual_address == 0 || resource_table.size == 0 { + return Ok(Vec::new()); + } + + let file_alignment = optional_header.windows_fields.file_alignment; + let sections = pe.sections; + + let rsrc_offset = rva_to_file_offset( + §ions, + file_alignment, + &opts, + resource_table.virtual_address, + ) + .ok_or(WevtTemplateExtractError::UnmappedRva { + rva: resource_table.virtual_address, + })?; + + let rsrc_end = rsrc_offset + .checked_add(resource_table.size as usize) + .ok_or(WevtTemplateExtractError::MalformedPe { + message: "resource directory overflow", + })?; + + let rsrc = + pe_bytes + .get(rsrc_offset..rsrc_end) + .ok_or(WevtTemplateExtractError::MalformedPe { + message: "resource directory out of bounds", + })?; + + let root_entries = directory_entries(rsrc, 0)?; + + let mut wevt_template_entry = None; + for entry in root_entries { + if !entry.name_is_string() { + continue; + } + let name = parse_resource_name(rsrc, entry.name_offset() as usize)?; + if name == "WEVT_TEMPLATE" { + wevt_template_entry = Some(entry); + break; + } + } + + let Some(wevt_template_entry) = wevt_template_entry else { + return Ok(Vec::new()); + }; + if !wevt_template_entry.data_is_directory() { + return Ok(Vec::new()); + } + + let mut out = Vec::new(); + + let wevt_dir_offset = wevt_template_entry.offset_to_directory() as usize; + for resource_entry in directory_entries(rsrc, wevt_dir_offset)? { + if !resource_entry.data_is_directory() { + continue; + } + let resource_id = entry_identifier(resource_entry, rsrc)?; + + let lang_dir_offset = resource_entry.offset_to_directory() as usize; + for lang_entry in directory_entries(rsrc, lang_dir_offset)? { + if lang_entry.name_is_string() { + continue; + } + let lang_id = lang_entry.name_offset(); + + let Some(data_entry_offset) = lang_entry.offset_to_data() else { + continue; + }; + let data_entry = parse_resource_data_entry(rsrc, data_entry_offset as usize)?; + let data_rva = data_entry.offset_to_data; + let data_size = data_entry.size as usize; + if data_size == 0 { + continue; + } + + let data_offset = rva_to_file_offset(§ions, file_alignment, &opts, data_rva) + .ok_or(WevtTemplateExtractError::UnmappedRva { rva: data_rva })?; + + let data_end = data_offset.checked_add(data_size).ok_or( + WevtTemplateExtractError::MalformedPe { + message: "resource data overflow", + }, + )?; + let data = pe_bytes + .get(data_offset..data_end) + .ok_or(WevtTemplateExtractError::MalformedPe { + message: "resource data out of bounds", + })? + .to_vec(); + + out.push(WevtTemplateResource { + resource: resource_id.clone(), + lang_id, + data, + }); + } + } + + Ok(out) +} diff --git a/src/wevt_templates/manifest/error.rs b/src/wevt_templates/manifest/error.rs new file mode 100644 index 00000000..89942355 --- /dev/null +++ b/src/wevt_templates/manifest/error.rs @@ -0,0 +1,48 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum WevtManifestError { + #[error("invalid signature at offset {offset}: expected {expected:?}, got {found:?}")] + InvalidSignature { + offset: u32, + expected: [u8; 4], + found: [u8; 4], + }, + + #[error("buffer too small for {what} at offset {offset} (need {need} bytes, have {have})")] + Truncated { + what: &'static str, + offset: u32, + need: usize, + have: usize, + }, + + #[error("offset {offset} out of bounds for {what} (len={len})")] + OffsetOutOfBounds { + what: &'static str, + offset: u32, + len: usize, + }, + + #[error("size {size} out of bounds for {what} at offset {offset}")] + SizeOutOfBounds { + what: &'static str, + offset: u32, + size: u32, + }, + + #[error("invalid count {count} for {what} at offset {offset}")] + CountOutOfBounds { + what: &'static str, + offset: u32, + count: u32, + }, + + #[error("invalid utf-16 string for {what} at offset {offset}")] + InvalidUtf16String { what: &'static str, offset: u32 }, + + #[error("invalid GUID for {what} at offset {offset}")] + InvalidGuid { what: &'static str, offset: u32 }, +} + +pub(super) type Result = std::result::Result; diff --git a/src/wevt_templates/manifest/mod.rs b/src/wevt_templates/manifest/mod.rs new file mode 100644 index 00000000..886dcf63 --- /dev/null +++ b/src/wevt_templates/manifest/mod.rs @@ -0,0 +1,37 @@ +//! Parsing for the `WEVT_TEMPLATE` resource payload (CRIM/WEVT/...). +//! +//! This module is a Rust port of the libyal/libfwevt "Windows Event manifest binary format" +//! documentation and aligns with the reference C implementation. +//! +//! Primary references: +//! - libfwevt: `documentation/Windows Event manifest binary format.asciidoc` +//! - MS-EVEN6: BinXml name hashing/layout and token grammar +//! +//! Design goals: +//! - Deterministic parsing (no signature scanning). +//! - Strict bounds/sanity checks; offsets are validated relative to the CRIM blob. +//! - Preserve unknown fields as raw integers/bytes (do not guess semantics). +//! - Provide stable join keys: provider GUID + event (id/version/...) + template offset. +//! +//! Note: libfwevt's map parsing is marked TODO; we parse VMAP per spec and keep unknown map +//! types as raw bytes. +//! +//! This module is split into: +//! - `types`: a typed view of the manifest structures (kept stable for downstream join/render code) +//! - `parse`: spec-backed parsing and bounds validation +//! - `error`: a small error enum that makes failures actionable in tests/tooling +//! +//! References: +//! - `docs/wevt_templates.md` (project notes + curated links) +//! - libfwevt manifest spec doc (CRIM/WEVT/EVNT/TTBL/TEMP) +//! - MS-EVEN6 (BinXml grammar notes used by template rendering) + +mod error; +mod parse; +mod types; +mod util; + +pub use error::WevtManifestError; +pub use types::*; + + diff --git a/src/wevt_templates/manifest/parse.rs b/src/wevt_templates/manifest/parse.rs new file mode 100644 index 00000000..c254a8a4 --- /dev/null +++ b/src/wevt_templates/manifest/parse.rs @@ -0,0 +1,1404 @@ +use std::collections::HashMap; + +use winstructs::guid::Guid; + +use super::error::{Result, WevtManifestError}; +use super::types::*; +use super::util::*; + +impl<'a> CrimManifest<'a> { + /// Parse a CRIM manifest blob (the payload stored inside a `WEVT_TEMPLATE` resource). + /// + /// This is the entrypoint for turning raw bytes into typed structures that can be joined + /// against EVTX event metadata (e.g. event→template lookups for offline caches). + pub fn parse(data: &'a [u8]) -> Result { + let header = parse_crim_header(data)?; + let crim_size_usize = + usize::try_from(header.size).map_err(|_| WevtManifestError::SizeOutOfBounds { + what: "CRIM.size", + offset: 0, + size: header.size, + })?; + if crim_size_usize > data.len() { + return Err(WevtManifestError::SizeOutOfBounds { + what: "CRIM.size", + offset: 0, + size: header.size, + }); + } + + let data = &data[..crim_size_usize]; + + let provider_count = usize::try_from(header.provider_count).map_err(|_| { + WevtManifestError::CountOutOfBounds { + what: "CRIM.provider_count", + offset: 12, + count: header.provider_count, + } + })?; + + let providers_off = 16usize; + let provider_desc_size = 20usize; + let providers_end = providers_off + .checked_add(provider_count.checked_mul(provider_desc_size).ok_or( + WevtManifestError::CountOutOfBounds { + what: "CRIM.provider_count", + offset: 12, + count: header.provider_count, + }, + )?) + .ok_or(WevtManifestError::CountOutOfBounds { + what: "CRIM.provider_count", + offset: 12, + count: header.provider_count, + })?; + + if providers_end > data.len() { + return Err(WevtManifestError::Truncated { + what: "CRIM provider descriptor array", + offset: 16, + need: providers_end - providers_off, + have: data.len().saturating_sub(providers_off), + }); + } + + let mut providers = Vec::with_capacity(provider_count); + for i in 0..provider_count { + let desc_off = providers_off + i * provider_desc_size; + let guid = read_guid_named(data, desc_off, "CRIM.provider.guid")?; + let provider_off = read_u32_named(data, desc_off + 16, "CRIM.provider.offset")?; + + let provider = parse_provider(data, guid, provider_off)?; + providers.push(provider); + } + + Ok(Self { + data, + header, + providers, + }) + } + + /// Build lookup indices to support joining events/templates. + /// + /// This is primarily used by cache builders and tooling: it yields stable keys for mapping + /// provider event definitions to template GUIDs, and for resolving templates by GUID. + pub fn build_index(&'a self) -> CrimManifestIndex<'a> { + let mut templates_by_guid: HashMap>> = HashMap::new(); + let mut event_to_template_guids: HashMap> = HashMap::new(); + + for provider in &self.providers { + if let Some(ttbl) = provider.wevt.elements.templates.as_ref() { + for tpl in &ttbl.templates { + templates_by_guid + .entry(tpl.guid.to_string()) + .or_default() + .push(tpl); + } + } + + if let Some(evnt) = provider.wevt.elements.events.as_ref() { + for ev in &evnt.events { + let Some(template_offset) = ev.template_offset else { + continue; + }; + + let Some(tpl) = provider.template_by_offset(template_offset) else { + continue; + }; + + let key = EventKey { + provider_guid: provider.guid.to_string(), + event_id: ev.identifier, + version: ev.version, + channel: ev.channel, + level: ev.level, + opcode: ev.opcode, + task: ev.task, + keywords: ev.keywords, + }; + + let entry = event_to_template_guids.entry(key).or_default(); + if !entry.contains(&tpl.guid) { + entry.push(tpl.guid.clone()); + } + } + } + } + + CrimManifestIndex { + templates_by_guid, + event_to_template_guids, + } + } +} + +fn parse_crim_header(data: &[u8]) -> Result { + let sig = read_sig_named(data, 0, "CRIM signature")?; + if sig != *b"CRIM" { + return Err(WevtManifestError::InvalidSignature { + offset: 0, + expected: *b"CRIM", + found: sig, + }); + } + + let size = read_u32_named(data, 4, "CRIM.size")?; + let major_version = read_u16_named(data, 8, "CRIM.major_version")?; + let minor_version = read_u16_named(data, 10, "CRIM.minor_version")?; + let provider_count = read_u32_named(data, 12, "CRIM.provider_count")?; + + if size < 16 { + return Err(WevtManifestError::SizeOutOfBounds { + what: "CRIM.size", + offset: 0, + size, + }); + } + + Ok(CrimHeader { + size, + major_version, + minor_version, + provider_count, + }) +} + +fn parse_provider<'a>(crim: &'a [u8], guid: Guid, provider_off: u32) -> Result> { + let provider_off_usize = u32_to_usize(provider_off, "WEVT provider offset", crim.len())?; + // Need at least 20 bytes for WEVT header. + require_len(crim, provider_off_usize, 20, "WEVT header")?; + + let sig = read_sig_named(crim, provider_off_usize, "WEVT signature")?; + if sig != *b"WEVT" { + return Err(WevtManifestError::InvalidSignature { + offset: provider_off, + expected: *b"WEVT", + found: sig, + }); + } + + let size = read_u32_named(crim, provider_off_usize + 4, "WEVT.size")?; + let message_identifier_raw = + read_u32_named(crim, provider_off_usize + 8, "WEVT.message_identifier")?; + let descriptor_count = + read_u32_named(crim, provider_off_usize + 12, "WEVT.number_of_descriptors")?; + let unknown2_count = read_u32_named(crim, provider_off_usize + 16, "WEVT.number_of_unknown2")?; + + let message_identifier = if message_identifier_raw == 0xffffffff { + None + } else { + Some(message_identifier_raw) + }; + + let desc_count_usize = + usize::try_from(descriptor_count).map_err(|_| WevtManifestError::CountOutOfBounds { + what: "WEVT.number_of_descriptors", + offset: provider_off + 12, + count: descriptor_count, + })?; + + let desc_off = provider_off_usize + 20; + let desc_bytes = + desc_count_usize + .checked_mul(8) + .ok_or(WevtManifestError::CountOutOfBounds { + what: "WEVT.number_of_descriptors", + offset: provider_off + 12, + count: descriptor_count, + })?; + require_len(crim, desc_off, desc_bytes, "WEVT descriptor array")?; + + let mut element_descriptors = Vec::with_capacity(desc_count_usize); + for i in 0..desc_count_usize { + let off = desc_off + i * 8; + let element_offset = read_u32_named(crim, off, "WEVT.descriptor.element_offset")?; + let unknown = read_u32_named(crim, off + 4, "WEVT.descriptor.unknown")?; + let element_off_usize = u32_to_usize(element_offset, "WEVT element offset", crim.len())?; + require_len(crim, element_off_usize, 4, "WEVT element signature")?; + let signature = read_sig_named(crim, element_off_usize, "WEVT element signature")?; + element_descriptors.push(ProviderElementDescriptor { + element_offset, + unknown, + signature, + }); + } + + let unknown2_count_usize = + usize::try_from(unknown2_count).map_err(|_| WevtManifestError::CountOutOfBounds { + what: "WEVT.number_of_unknown2", + offset: provider_off + 16, + count: unknown2_count, + })?; + + let unknown2_off = desc_off + desc_bytes; + let unknown2_bytes = + unknown2_count_usize + .checked_mul(4) + .ok_or(WevtManifestError::CountOutOfBounds { + what: "WEVT.number_of_unknown2", + offset: provider_off + 16, + count: unknown2_count, + })?; + require_len(crim, unknown2_off, unknown2_bytes, "WEVT unknown2 array")?; + + let mut unknown2 = Vec::with_capacity(unknown2_count_usize); + for i in 0..unknown2_count_usize { + let off = unknown2_off + i * 4; + unknown2.push(read_u32_named(crim, off, "WEVT.unknown2")?); + } + + let elements = parse_provider_elements(crim, &element_descriptors)?; + + Ok(Provider { + guid, + offset: provider_off, + wevt: WevtProvider { + offset: provider_off, + size, + message_identifier, + element_descriptors, + unknown2, + elements, + }, + }) +} + +fn parse_provider_elements<'a>( + crim: &'a [u8], + descriptors: &[ProviderElementDescriptor], +) -> Result> { + let mut out = ProviderElements::default(); + + for d in descriptors { + match &d.signature { + b"CHAN" => { + out.channels = Some(parse_channels(crim, d.element_offset)?); + } + b"EVNT" => { + out.events = Some(parse_events(crim, d.element_offset)?); + } + b"KEYW" => { + out.keywords = Some(parse_keywords(crim, d.element_offset)?); + } + b"LEVL" => { + out.levels = Some(parse_levels(crim, d.element_offset)?); + } + b"MAPS" => { + out.maps = Some(parse_maps(crim, d.element_offset)?); + } + b"OPCO" => { + out.opcodes = Some(parse_opcodes(crim, d.element_offset)?); + } + b"TASK" => { + out.tasks = Some(parse_tasks(crim, d.element_offset)?); + } + b"TTBL" => { + out.templates = Some(parse_ttbl(crim, d.element_offset)?); + } + _ => { + // Unknown element: try to read size (offset+4) and capture the region. + let off = u32_to_usize(d.element_offset, "provider element offset", crim.len())?; + if off + 8 <= crim.len() { + let size = read_u32_named(crim, off + 4, "provider element size")?; + let end = u32_to_usize( + d.element_offset.saturating_add(size), + "unknown element end", + crim.len(), + )?; + let data = &crim[off..end]; + out.unknown.push(UnknownElement { + signature: d.signature, + offset: d.element_offset, + size, + data, + }); + } else { + return Err(WevtManifestError::Truncated { + what: "unknown element header", + offset: d.element_offset, + need: 8, + have: crim.len().saturating_sub(off), + }); + } + } + } + } + + Ok(out) +} + +fn parse_channels(crim: &[u8], off: u32) -> Result { + let off_usize = u32_to_usize(off, "CHAN offset", crim.len())?; + require_len(crim, off_usize, 12, "CHAN header")?; + let sig = read_sig_named(crim, off_usize, "CHAN signature")?; + if sig != *b"CHAN" { + return Err(WevtManifestError::InvalidSignature { + offset: off, + expected: *b"CHAN", + found: sig, + }); + } + let size = read_u32_named(crim, off_usize + 4, "CHAN.size")?; + let count = read_u32_named(crim, off_usize + 8, "CHAN.count")?; + + let count_usize = usize::try_from(count).map_err(|_| WevtManifestError::CountOutOfBounds { + what: "CHAN.count", + offset: off + 8, + count, + })?; + let defs_off = off_usize + 12; + let defs_bytes = count_usize + .checked_mul(16) + .ok_or(WevtManifestError::CountOutOfBounds { + what: "CHAN.count", + offset: off + 8, + count, + })?; + let min_end = defs_off + .checked_add(defs_bytes) + .ok_or(WevtManifestError::SizeOutOfBounds { + what: "CHAN definitions array", + offset: off, + size, + })?; + + let _end = if size == 0 { + // libfwevt accepts size==0 and uses `count` to parse the definitions array. + min_end + } else { + if size < 12 { + return Err(WevtManifestError::SizeOutOfBounds { + what: "CHAN.size", + offset: off, + size, + }); + } + let end = checked_end(crim.len(), off, size, "CHAN.size")?; + if min_end > end { + return Err(WevtManifestError::SizeOutOfBounds { + what: "CHAN definitions array", + offset: off, + size, + }); + } + end + }; + + let mut channels = Vec::with_capacity(count_usize); + for i in 0..count_usize { + let d_off = defs_off + i * 16; + let identifier = read_u32_named(crim, d_off, "CHAN.identifier")?; + let name_offset = read_u32_named(crim, d_off + 4, "CHAN.name_offset")?; + let unknown = read_u32_named(crim, d_off + 8, "CHAN.unknown")?; + let msg_raw = read_u32_named(crim, d_off + 12, "CHAN.message_identifier")?; + let message_identifier = if msg_raw == 0xffffffff { + None + } else { + Some(msg_raw) + }; + let name = if name_offset == 0 { + None + } else { + Some(read_sized_utf16_string(crim, name_offset, "CHAN name")?) + }; + channels.push(ChannelDefinition { + identifier, + name_offset, + unknown, + message_identifier, + name, + }); + } + + Ok(ChannelDefinitions { + offset: off, + size, + channels, + }) +} + +fn parse_events(crim: &[u8], off: u32) -> Result { + let off_usize = u32_to_usize(off, "EVNT offset", crim.len())?; + require_len(crim, off_usize, 16, "EVNT header")?; + let sig = read_sig_named(crim, off_usize, "EVNT signature")?; + if sig != *b"EVNT" { + return Err(WevtManifestError::InvalidSignature { + offset: off, + expected: *b"EVNT", + found: sig, + }); + } + let size = read_u32_named(crim, off_usize + 4, "EVNT.size")?; + let count = read_u32_named(crim, off_usize + 8, "EVNT.count")?; + let unknown = read_u32_named(crim, off_usize + 12, "EVNT.unknown")?; + + let count_usize = usize::try_from(count).map_err(|_| WevtManifestError::CountOutOfBounds { + what: "EVNT.count", + offset: off + 8, + count, + })?; + let events_off = off_usize + 16; + let events_bytes = count_usize + .checked_mul(48) + .ok_or(WevtManifestError::CountOutOfBounds { + what: "EVNT.count", + offset: off + 8, + count, + })?; + let min_end = + events_off + .checked_add(events_bytes) + .ok_or(WevtManifestError::SizeOutOfBounds { + what: "EVNT event array", + offset: off, + size, + })?; + + let end = if size == 0 { + // libfwevt accepts size==0 and uses `count` to parse the array. + min_end + } else { + if size < 16 { + return Err(WevtManifestError::SizeOutOfBounds { + what: "EVNT.size", + offset: off, + size, + }); + } + let end = checked_end(crim.len(), off, size, "EVNT.size")?; + if min_end > end { + return Err(WevtManifestError::SizeOutOfBounds { + what: "EVNT event array", + offset: off, + size, + }); + } + end + }; + + let mut events = Vec::with_capacity(count_usize); + for i in 0..count_usize { + let e_off = events_off + i * 48; + let identifier = read_u16_named(crim, e_off, "EVNT.event.identifier")?; + let version = read_u8_named(crim, e_off + 2, "EVNT.event.version")?; + let channel = read_u8_named(crim, e_off + 3, "EVNT.event.channel")?; + let level = read_u8_named(crim, e_off + 4, "EVNT.event.level")?; + let opcode = read_u8_named(crim, e_off + 5, "EVNT.event.opcode")?; + let task = read_u16_named(crim, e_off + 6, "EVNT.event.task")?; + let keywords = read_u64_named(crim, e_off + 8, "EVNT.event.keywords")?; + let message_identifier = read_u32_named(crim, e_off + 16, "EVNT.event.message_identifier")?; + let template_offset_raw = read_u32_named(crim, e_off + 20, "EVNT.event.template_offset")?; + let opcode_offset_raw = read_u32_named(crim, e_off + 24, "EVNT.event.opcode_offset")?; + let level_offset_raw = read_u32_named(crim, e_off + 28, "EVNT.event.level_offset")?; + let task_offset_raw = read_u32_named(crim, e_off + 32, "EVNT.event.task_offset")?; + let unknown_count = read_u32_named(crim, e_off + 36, "EVNT.event.unknown_count")?; + let unknown_offset = read_u32_named(crim, e_off + 40, "EVNT.event.unknown_offset")?; + let flags = read_u32_named(crim, e_off + 44, "EVNT.event.flags")?; + + events.push(EventDefinition { + identifier, + version, + channel, + level, + opcode, + task, + keywords, + message_identifier, + template_offset: if template_offset_raw == 0 { + None + } else { + Some(template_offset_raw) + }, + opcode_offset: if opcode_offset_raw == 0 { + None + } else { + Some(opcode_offset_raw) + }, + level_offset: if level_offset_raw == 0 { + None + } else { + Some(level_offset_raw) + }, + task_offset: if task_offset_raw == 0 { + None + } else { + Some(task_offset_raw) + }, + unknown_count, + unknown_offset, + flags, + }); + } + + let trailing = if end >= events_off + events_bytes { + crim[events_off + events_bytes..end].to_vec() + } else { + vec![] + }; + + Ok(EventDefinitions { + offset: off, + size, + unknown, + events, + trailing, + }) +} + +fn parse_keywords(crim: &[u8], off: u32) -> Result { + let off_usize = u32_to_usize(off, "KEYW offset", crim.len())?; + require_len(crim, off_usize, 12, "KEYW header")?; + let sig = read_sig_named(crim, off_usize, "KEYW signature")?; + if sig != *b"KEYW" { + return Err(WevtManifestError::InvalidSignature { + offset: off, + expected: *b"KEYW", + found: sig, + }); + } + let size = read_u32_named(crim, off_usize + 4, "KEYW.size")?; + let count = read_u32_named(crim, off_usize + 8, "KEYW.count")?; + + let count_usize = usize::try_from(count).map_err(|_| WevtManifestError::CountOutOfBounds { + what: "KEYW.count", + offset: off + 8, + count, + })?; + let defs_off = off_usize + 12; + let defs_bytes = count_usize + .checked_mul(16) + .ok_or(WevtManifestError::CountOutOfBounds { + what: "KEYW.count", + offset: off + 8, + count, + })?; + let min_end = defs_off + .checked_add(defs_bytes) + .ok_or(WevtManifestError::SizeOutOfBounds { + what: "KEYW definitions array", + offset: off, + size, + })?; + + let _end = if size == 0 { + // libfwevt accepts size==0 and uses `count` to parse the definitions array. + min_end + } else { + if size < 12 { + return Err(WevtManifestError::SizeOutOfBounds { + what: "KEYW.size", + offset: off, + size, + }); + } + let end = checked_end(crim.len(), off, size, "KEYW.size")?; + if min_end > end { + return Err(WevtManifestError::SizeOutOfBounds { + what: "KEYW definitions array", + offset: off, + size, + }); + } + end + }; + + let mut keywords = Vec::with_capacity(count_usize); + for i in 0..count_usize { + let d_off = defs_off + i * 16; + let identifier = read_u64_named(crim, d_off, "KEYW.identifier")?; + let msg_raw = read_u32_named(crim, d_off + 8, "KEYW.message_identifier")?; + let data_offset = read_u32_named(crim, d_off + 12, "KEYW.data_offset")?; + let message_identifier = if msg_raw == 0xffffffff { + None + } else { + Some(msg_raw) + }; + let name = if data_offset == 0 { + None + } else { + Some(read_sized_utf16_string(crim, data_offset, "KEYW data")?) + }; + keywords.push(KeywordDefinition { + identifier, + message_identifier, + data_offset, + name, + }); + } + + Ok(KeywordDefinitions { + offset: off, + size, + keywords, + }) +} + +fn parse_levels(crim: &[u8], off: u32) -> Result { + let off_usize = u32_to_usize(off, "LEVL offset", crim.len())?; + require_len(crim, off_usize, 12, "LEVL header")?; + let sig = read_sig_named(crim, off_usize, "LEVL signature")?; + if sig != *b"LEVL" { + return Err(WevtManifestError::InvalidSignature { + offset: off, + expected: *b"LEVL", + found: sig, + }); + } + let size = read_u32_named(crim, off_usize + 4, "LEVL.size")?; + let count = read_u32_named(crim, off_usize + 8, "LEVL.count")?; + + let count_usize = usize::try_from(count).map_err(|_| WevtManifestError::CountOutOfBounds { + what: "LEVL.count", + offset: off + 8, + count, + })?; + let defs_off = off_usize + 12; + let defs_bytes = count_usize + .checked_mul(12) + .ok_or(WevtManifestError::CountOutOfBounds { + what: "LEVL.count", + offset: off + 8, + count, + })?; + let min_end = defs_off + .checked_add(defs_bytes) + .ok_or(WevtManifestError::SizeOutOfBounds { + what: "LEVL definitions array", + offset: off, + size, + })?; + + let _end = if size == 0 { + min_end + } else { + if size < 12 { + return Err(WevtManifestError::SizeOutOfBounds { + what: "LEVL.size", + offset: off, + size, + }); + } + let end = checked_end(crim.len(), off, size, "LEVL.size")?; + if min_end > end { + return Err(WevtManifestError::SizeOutOfBounds { + what: "LEVL definitions array", + offset: off, + size, + }); + } + end + }; + + let mut levels = Vec::with_capacity(count_usize); + for i in 0..count_usize { + let d_off = defs_off + i * 12; + let identifier = read_u32_named(crim, d_off, "LEVL.identifier")?; + let msg_raw = read_u32_named(crim, d_off + 4, "LEVL.message_identifier")?; + let data_offset = read_u32_named(crim, d_off + 8, "LEVL.data_offset")?; + let message_identifier = if msg_raw == 0xffffffff { + None + } else { + Some(msg_raw) + }; + let name = if data_offset == 0 { + None + } else { + Some(read_sized_utf16_string(crim, data_offset, "LEVL data")?) + }; + levels.push(LevelDefinition { + identifier, + message_identifier, + data_offset, + name, + }); + } + + Ok(LevelDefinitions { + offset: off, + size, + levels, + }) +} + +fn parse_opcodes(crim: &[u8], off: u32) -> Result { + let off_usize = u32_to_usize(off, "OPCO offset", crim.len())?; + require_len(crim, off_usize, 12, "OPCO header")?; + let sig = read_sig_named(crim, off_usize, "OPCO signature")?; + if sig != *b"OPCO" { + return Err(WevtManifestError::InvalidSignature { + offset: off, + expected: *b"OPCO", + found: sig, + }); + } + let size = read_u32_named(crim, off_usize + 4, "OPCO.size")?; + let count = read_u32_named(crim, off_usize + 8, "OPCO.count")?; + + let count_usize = usize::try_from(count).map_err(|_| WevtManifestError::CountOutOfBounds { + what: "OPCO.count", + offset: off + 8, + count, + })?; + let defs_off = off_usize + 12; + let defs_bytes = count_usize + .checked_mul(12) + .ok_or(WevtManifestError::CountOutOfBounds { + what: "OPCO.count", + offset: off + 8, + count, + })?; + let min_end = defs_off + .checked_add(defs_bytes) + .ok_or(WevtManifestError::SizeOutOfBounds { + what: "OPCO definitions array", + offset: off, + size, + })?; + + let _end = if size == 0 { + min_end + } else { + if size < 12 { + return Err(WevtManifestError::SizeOutOfBounds { + what: "OPCO.size", + offset: off, + size, + }); + } + let end = checked_end(crim.len(), off, size, "OPCO.size")?; + if min_end > end { + return Err(WevtManifestError::SizeOutOfBounds { + what: "OPCO definitions array", + offset: off, + size, + }); + } + end + }; + + let mut opcodes = Vec::with_capacity(count_usize); + for i in 0..count_usize { + let d_off = defs_off + i * 12; + let identifier = read_u32_named(crim, d_off, "OPCO.identifier")?; + let msg_raw = read_u32_named(crim, d_off + 4, "OPCO.message_identifier")?; + let data_offset = read_u32_named(crim, d_off + 8, "OPCO.data_offset")?; + let message_identifier = if msg_raw == 0xffffffff { + None + } else { + Some(msg_raw) + }; + let name = if data_offset == 0 { + None + } else { + Some(read_sized_utf16_string(crim, data_offset, "OPCO data")?) + }; + opcodes.push(OpcodeDefinition { + identifier, + message_identifier, + data_offset, + name, + }); + } + + Ok(OpcodeDefinitions { + offset: off, + size, + opcodes, + }) +} + +fn parse_tasks(crim: &[u8], off: u32) -> Result { + let off_usize = u32_to_usize(off, "TASK offset", crim.len())?; + require_len(crim, off_usize, 12, "TASK header")?; + let sig = read_sig_named(crim, off_usize, "TASK signature")?; + if sig != *b"TASK" { + return Err(WevtManifestError::InvalidSignature { + offset: off, + expected: *b"TASK", + found: sig, + }); + } + let size = read_u32_named(crim, off_usize + 4, "TASK.size")?; + let count = read_u32_named(crim, off_usize + 8, "TASK.count")?; + + let count_usize = usize::try_from(count).map_err(|_| WevtManifestError::CountOutOfBounds { + what: "TASK.count", + offset: off + 8, + count, + })?; + let defs_off = off_usize + 12; + let defs_bytes = count_usize + .checked_mul(28) + .ok_or(WevtManifestError::CountOutOfBounds { + what: "TASK.count", + offset: off + 8, + count, + })?; + let min_end = defs_off + .checked_add(defs_bytes) + .ok_or(WevtManifestError::SizeOutOfBounds { + what: "TASK definitions array", + offset: off, + size, + })?; + + let _end = if size == 0 { + min_end + } else { + if size < 12 { + return Err(WevtManifestError::SizeOutOfBounds { + what: "TASK.size", + offset: off, + size, + }); + } + let end = checked_end(crim.len(), off, size, "TASK.size")?; + if min_end > end { + return Err(WevtManifestError::SizeOutOfBounds { + what: "TASK definitions array", + offset: off, + size, + }); + } + end + }; + + let mut tasks = Vec::with_capacity(count_usize); + for i in 0..count_usize { + let d_off = defs_off + i * 28; + let identifier = read_u32_named(crim, d_off, "TASK.identifier")?; + let msg_raw = read_u32_named(crim, d_off + 4, "TASK.message_identifier")?; + let mui_identifier = read_guid_named(crim, d_off + 8, "TASK.mui_identifier")?; + let data_offset = read_u32_named(crim, d_off + 24, "TASK.data_offset")?; + let message_identifier = if msg_raw == 0xffffffff { + None + } else { + Some(msg_raw) + }; + let name = if data_offset == 0 { + None + } else { + Some(read_sized_utf16_string(crim, data_offset, "TASK data")?) + }; + tasks.push(TaskDefinition { + identifier, + message_identifier, + mui_identifier, + data_offset, + name, + }); + } + + Ok(TaskDefinitions { + offset: off, + size, + tasks, + }) +} + +fn parse_ttbl<'a>(crim: &'a [u8], off: u32) -> Result> { + let off_usize = u32_to_usize(off, "TTBL offset", crim.len())?; + require_len(crim, off_usize, 12, "TTBL header")?; + let sig = read_sig_named(crim, off_usize, "TTBL signature")?; + if sig != *b"TTBL" { + return Err(WevtManifestError::InvalidSignature { + offset: off, + expected: *b"TTBL", + found: sig, + }); + } + let size = read_u32_named(crim, off_usize + 4, "TTBL.size")?; + let count = read_u32_named(crim, off_usize + 8, "TTBL.count")?; + let end = if size == 0 { + // libfwevt accepts size==0 and parses by `count` and per-template sizes. + crim.len() + } else { + if size < 12 { + return Err(WevtManifestError::SizeOutOfBounds { + what: "TTBL.size", + offset: off, + size, + }); + } + checked_end(crim.len(), off, size, "TTBL.size")? + }; + + let count_usize = usize::try_from(count).map_err(|_| WevtManifestError::CountOutOfBounds { + what: "TTBL.count", + offset: off + 8, + count, + })?; + + let mut templates = Vec::with_capacity(count_usize); + let mut cur = off_usize + 12; + + for _ in 0..count_usize { + if cur + 40 > end { + return Err(WevtManifestError::Truncated { + what: "TEMP header", + offset: usize_to_u32(cur), + need: 40, + have: end.saturating_sub(cur), + }); + } + let temp_sig = read_sig_named(crim, cur, "TEMP signature")?; + if temp_sig != *b"TEMP" { + return Err(WevtManifestError::InvalidSignature { + offset: usize_to_u32(cur), + expected: *b"TEMP", + found: temp_sig, + }); + } + let temp_size = read_u32_named(crim, cur + 4, "TEMP.size")?; + if temp_size < 40 { + return Err(WevtManifestError::SizeOutOfBounds { + what: "TEMP.size", + offset: usize_to_u32(cur), + size: temp_size, + }); + } + let temp_end = checked_end(end, usize_to_u32(cur), temp_size, "TEMP.size")?; + let temp_off_u32 = usize_to_u32(cur); + + let item_descriptor_count = read_u32_named(crim, cur + 8, "TEMP.item_descriptor_count")?; + let item_name_count = read_u32_named(crim, cur + 12, "TEMP.item_name_count")?; + let template_items_offset = read_u32_named(crim, cur + 16, "TEMP.template_items_offset")?; + let event_type = read_u32_named(crim, cur + 20, "TEMP.event_type")?; + let guid = read_guid_named(crim, cur + 24, "TEMP.guid")?; + + // libfwevt notes: if number_of_descriptors (and number_of_names) is 0, the template_items_offset + // is either 0 or points to the end of the template. Treat non-zero name count in this case as invalid. + if item_descriptor_count == 0 && item_name_count != 0 { + return Err(WevtManifestError::CountOutOfBounds { + what: "TEMP.item_name_count (expected 0 when item_descriptor_count == 0)", + offset: temp_off_u32 + 12, + count: item_name_count, + }); + } + + let template_slice = &crim[cur..temp_end]; + + // Compute binxml bounds using template_items_offset (absolute, relative to CRIM). + let items_abs = if item_descriptor_count == 0 && template_items_offset == 0 { + // libfwevt allows 0 in the no-items case; treat as end-of-template for binxml sizing. + temp_off_u32.saturating_add(temp_size) + } else { + template_items_offset + }; + + let items_rel = if items_abs == 0 { + // No guidance; treat items as starting at end-of-template. + temp_size + } else if items_abs < temp_off_u32 { + return Err(WevtManifestError::OffsetOutOfBounds { + what: "TEMP.template_items_offset", + offset: items_abs, + len: crim.len(), + }); + } else { + items_abs - temp_off_u32 + }; + + let items_rel_usize = u32_to_usize( + items_rel, + "TEMP.template_items_offset (relative)", + template_slice.len(), + )?; + if items_rel_usize > template_slice.len() { + return Err(WevtManifestError::OffsetOutOfBounds { + what: "TEMP.template_items_offset (relative)", + offset: temp_off_u32.saturating_add(items_rel), + len: crim.len(), + }); + } + + let binxml_start = 40usize; + let binxml_end = items_rel_usize.min(template_slice.len()); + let binxml = if binxml_end >= binxml_start { + &template_slice[binxml_start..binxml_end] + } else { + &template_slice[binxml_start..binxml_start] + }; + + let items = parse_template_items( + template_slice, + temp_off_u32, + item_descriptor_count, + template_items_offset, + )?; + + templates.push(TemplateDefinition { + offset: temp_off_u32, + size: temp_size, + item_descriptor_count, + item_name_count, + template_items_offset, + event_type, + guid, + binxml, + items, + }); + + cur = temp_end; + } + + Ok(TemplateTable { + offset: off, + size, + templates, + }) +} + +fn parse_template_items( + template: &[u8], + template_off_abs: u32, + item_descriptor_count: u32, + template_items_offset_abs: u32, +) -> Result> { + let count_usize = usize::try_from(item_descriptor_count).map_err(|_| { + WevtManifestError::CountOutOfBounds { + what: "TEMP.item_descriptor_count", + offset: template_off_abs + 8, + count: item_descriptor_count, + } + })?; + + if count_usize == 0 { + // Validate template_items_offset for the zero-items case. + if template_items_offset_abs != 0 + && template_items_offset_abs != template_off_abs.saturating_add(template.len() as u32) + { + return Err(WevtManifestError::OffsetOutOfBounds { + what: "TEMP.template_items_offset (expected 0 or end-of-template when item_descriptor_count==0)", + offset: template_items_offset_abs, + len: template_off_abs.saturating_add(template.len() as u32) as usize, + }); + } + return Ok(vec![]); + } + + if template_items_offset_abs < template_off_abs { + return Err(WevtManifestError::OffsetOutOfBounds { + what: "TEMP.template_items_offset", + offset: template_items_offset_abs, + len: template_off_abs.saturating_add(template.len() as u32) as usize, + }); + } + + let rel = template_items_offset_abs - template_off_abs; + let rel_usize = u32_to_usize(rel, "TEMP.template_items_offset (relative)", template.len())?; + if rel_usize < 40 || rel_usize >= template.len() { + return Err(WevtManifestError::OffsetOutOfBounds { + what: "TEMP.template_items_offset (relative)", + offset: template_items_offset_abs, + len: template_off_abs.saturating_add(template.len() as u32) as usize, + }); + } + + let needed = count_usize + .checked_mul(20) + .ok_or(WevtManifestError::CountOutOfBounds { + what: "TEMP.item_descriptor_count", + offset: template_off_abs + 8, + count: item_descriptor_count, + })?; + if rel_usize + needed > template.len() { + return Err(WevtManifestError::Truncated { + what: "template item descriptors", + offset: template_items_offset_abs, + need: needed, + have: template.len().saturating_sub(rel_usize), + }); + } + + let descriptor_end = rel_usize + needed; + + // First pass: parse descriptors and collect the minimal non-zero name offset (relative to template base). + let mut items = Vec::with_capacity(count_usize); + let mut min_name_rel: Option = None; + + for i in 0..count_usize { + let d_off = rel_usize + i * 20; + let unknown1 = read_u32_named(template, d_off, "TEMP.item.unknown1")?; + let input_type = read_u8_named(template, d_off + 4, "TEMP.item.input_type")?; + let output_type = read_u8_named(template, d_off + 5, "TEMP.item.output_type")?; + let unknown3 = read_u16_named(template, d_off + 6, "TEMP.item.unknown3")?; + let unknown4 = read_u32_named(template, d_off + 8, "TEMP.item.unknown4")?; + let count = read_u16_named(template, d_off + 12, "TEMP.item.count")?; + let length = read_u16_named(template, d_off + 14, "TEMP.item.length")?; + let name_offset = read_u32_named(template, d_off + 16, "TEMP.item.name_offset")?; + + if name_offset != 0 { + if name_offset < template_off_abs { + return Err(WevtManifestError::OffsetOutOfBounds { + what: "template item name_offset", + offset: name_offset, + len: template_off_abs.saturating_add(template.len() as u32) as usize, + }); + } + let name_rel = name_offset - template_off_abs; + let name_rel_usize = u32_to_usize( + name_rel, + "template item name_offset (relative)", + template.len(), + )?; + min_name_rel = Some(min_name_rel.map_or(name_rel_usize, |m| m.min(name_rel_usize))); + } + + items.push(TemplateItem { + unknown1, + input_type, + output_type, + unknown3, + unknown4, + count, + length, + name_offset, + name: None, + }); + } + + // libfwevt’s reader relies on a boundary between descriptors and names; enforce that at least + // the first name (if present) starts after the descriptor table. + if let Some(min_name_rel) = min_name_rel + && min_name_rel < descriptor_end + { + return Err(WevtManifestError::OffsetOutOfBounds { + what: "template item name_offset overlaps descriptor table", + offset: template_off_abs.saturating_add(min_name_rel as u32), + len: template_off_abs.saturating_add(template.len() as u32) as usize, + }); + } + + // Second pass: resolve names. + for item in &mut items { + if item.name_offset == 0 { + continue; + } + let name_rel = item.name_offset - template_off_abs; + item.name = Some(read_sized_utf16_string( + template, + name_rel, + "template item name", + )?); + } + + Ok(items) +} + +fn parse_maps<'a>(crim: &'a [u8], off: u32) -> Result> { + // Maps parsing in libfwevt is TODO; we implement VMAP per spec and keep others opaque. + let off_usize = u32_to_usize(off, "MAPS offset", crim.len())?; + require_len(crim, off_usize, 16, "MAPS header")?; + let sig = read_sig_named(crim, off_usize, "MAPS signature")?; + if sig != *b"MAPS" { + return Err(WevtManifestError::InvalidSignature { + offset: off, + expected: *b"MAPS", + found: sig, + }); + } + let size = read_u32_named(crim, off_usize + 4, "MAPS.size")?; + let count = read_u32_named(crim, off_usize + 8, "MAPS.count")?; + let first_map_offset = read_u32_named(crim, off_usize + 12, "MAPS.first_map_offset")?; + let end = if size == 0 { + // libfwevt accepts size==0 and parses by offsets/count. + crim.len() + } else { + if size < 16 { + return Err(WevtManifestError::SizeOutOfBounds { + what: "MAPS.size", + offset: off, + size, + }); + } + checked_end(crim.len(), off, size, "MAPS.size")? + }; + + let count_usize = usize::try_from(count).map_err(|_| WevtManifestError::CountOutOfBounds { + what: "MAPS.count", + offset: off + 8, + count, + })?; + + let mut map_offsets: Vec = Vec::with_capacity(count_usize); + if count_usize > 0 { + // Interpret first_map_offset as map_offsets[0] when non-zero; otherwise fallback to implied offset. + let implied_first = (off_usize + 16 + (count_usize.saturating_sub(1) * 4)) as u32; + let first = if first_map_offset == 0 { + implied_first + } else { + first_map_offset + }; + map_offsets.push(first); + } + + // Remaining offsets array (count-1). + let offs_array_off = off_usize + 16; + let offs_array_bytes = count_usize.saturating_sub(1).checked_mul(4).unwrap_or(0); + if offs_array_off + offs_array_bytes > end { + return Err(WevtManifestError::SizeOutOfBounds { + what: "MAPS offsets array", + offset: off, + size, + }); + } + for i in 0..count_usize.saturating_sub(1) { + let o = read_u32_named(crim, offs_array_off + i * 4, "MAPS.map_offset")?; + map_offsets.push(o); + } + + // Parse each map by offset; boundaries are unknown, so for unknown map types we capture until next map offset or MAPS end. + let mut maps = Vec::with_capacity(count_usize); + for (i, &map_off) in map_offsets.iter().enumerate() { + let map_off_usize = u32_to_usize(map_off, "MAPS map offset", crim.len())?; + if map_off_usize + 4 > crim.len() { + return Err(WevtManifestError::Truncated { + what: "MAPS map signature", + offset: map_off, + need: 4, + have: crim.len().saturating_sub(map_off_usize), + }); + } + let sig = read_sig_named(crim, map_off_usize, "MAPS map signature")?; + let next_off = map_offsets + .get(i + 1) + .copied() + .unwrap_or_else(|| usize_to_u32(end)); + let next_usize = u32_to_usize(next_off, "MAPS map end", crim.len())?; + let slice_end = next_usize.min(end); + if slice_end < map_off_usize { + return Err(WevtManifestError::OffsetOutOfBounds { + what: "MAPS map boundary", + offset: next_off, + len: crim.len(), + }); + } + let map_slice = match &sig { + b"VMAP" => &crim[map_off_usize..slice_end], + // Avoid capturing an unbounded tail for unknown map types when MAPS.size == 0. + _ => &crim[map_off_usize..std::cmp::min(map_off_usize + 4, slice_end)], + }; + + match &sig { + b"VMAP" => { + maps.push(MapDefinition::ValueMap(parse_vmap( + crim, map_off, map_slice, + )?)); + } + b"BMAP" => { + maps.push(MapDefinition::Bitmap(BitmapMap { + offset: map_off, + data: map_slice, + })); + } + _ => { + maps.push(MapDefinition::Unknown { + signature: sig, + offset: map_off, + data: map_slice, + }); + } + } + } + + Ok(MapsDefinitions { + offset: off, + size, + maps, + }) +} + +fn parse_vmap<'a>(crim: &'a [u8], off: u32, map_slice: &'a [u8]) -> Result> { + // VMAP layout (per spec): + // 0:4 sig + // 4:4 size (including signature) + // 8:4 map_string_offset (relative to CRIM) + // 12:4 entry_count + // 16: entries (8 bytes each) + if map_slice.len() < 16 { + return Err(WevtManifestError::Truncated { + what: "VMAP header", + offset: off, + need: 16, + have: map_slice.len(), + }); + } + let size = read_u32_named(map_slice, 4, "VMAP.size")?; + let map_string_offset = read_u32_named(map_slice, 8, "VMAP.map_string_offset")?; + let entry_count = read_u32_named(map_slice, 12, "VMAP.entry_count")?; + + let size_usize = usize::try_from(size).map_err(|_| WevtManifestError::SizeOutOfBounds { + what: "VMAP.size", + offset: off, + size, + })?; + if size_usize < 16 || size_usize > map_slice.len() { + return Err(WevtManifestError::SizeOutOfBounds { + what: "VMAP.size", + offset: off, + size, + }); + } + + let entry_count_usize = + usize::try_from(entry_count).map_err(|_| WevtManifestError::CountOutOfBounds { + what: "VMAP.entry_count", + offset: off + 12, + count: entry_count, + })?; + let entries_bytes = + entry_count_usize + .checked_mul(8) + .ok_or(WevtManifestError::CountOutOfBounds { + what: "VMAP.entry_count", + offset: off + 12, + count: entry_count, + })?; + + if 16 + entries_bytes > size_usize { + return Err(WevtManifestError::SizeOutOfBounds { + what: "VMAP entries array", + offset: off, + size, + }); + } + + let mut entries = Vec::with_capacity(entry_count_usize); + for i in 0..entry_count_usize { + let e_off = 16 + i * 8; + let identifier = read_u32_named(map_slice, e_off, "VMAP.entry.identifier")?; + let msg_raw = read_u32_named(map_slice, e_off + 4, "VMAP.entry.message_identifier")?; + let message_identifier = if msg_raw == 0xffffffff { + None + } else { + Some(msg_raw) + }; + entries.push(ValueMapEntry { + identifier, + message_identifier, + }); + } + + let map_string = if map_string_offset == 0 { + None + } else { + Some(read_sized_utf16_string( + crim, + map_string_offset, + "VMAP map string", + )?) + }; + + let trailing = &map_slice[16 + entries_bytes..size_usize]; + + Ok(ValueMap { + offset: off, + size, + map_string_offset, + entries, + map_string, + trailing, + }) +} diff --git a/src/wevt_templates/manifest/types.rs b/src/wevt_templates/manifest/types.rs new file mode 100644 index 00000000..66a7ca38 --- /dev/null +++ b/src/wevt_templates/manifest/types.rs @@ -0,0 +1,282 @@ +use std::collections::HashMap; +use winstructs::guid::Guid; + +#[derive(Debug, Clone)] +pub struct CrimManifest<'a> { + /// Slice limited to CRIM.size (no trailing bytes). + pub data: &'a [u8], + pub header: CrimHeader, + pub providers: Vec>, +} + +#[derive(Debug, Clone)] +pub struct CrimHeader { + pub size: u32, + pub major_version: u16, + pub minor_version: u16, + pub provider_count: u32, +} + +#[derive(Debug, Clone)] +pub struct Provider<'a> { + pub guid: Guid, + /// Offset of the WEVT provider data, relative to the start of the CRIM blob. + pub offset: u32, + pub wevt: WevtProvider<'a>, +} + +#[derive(Debug, Clone)] +pub struct WevtProvider<'a> { + pub offset: u32, + pub size: u32, + pub message_identifier: Option, + pub element_descriptors: Vec, + pub unknown2: Vec, + pub elements: ProviderElements<'a>, +} + +#[derive(Debug, Clone)] +pub struct ProviderElementDescriptor { + /// Offset of the element (e.g. CHAN/EVNT/TTBL), relative to the start of the CRIM blob. + pub element_offset: u32, + pub unknown: u32, + pub signature: [u8; 4], +} + +#[derive(Debug, Clone, Default)] +pub struct ProviderElements<'a> { + pub channels: Option, + pub events: Option, + pub keywords: Option, + pub levels: Option, + pub maps: Option>, + pub opcodes: Option, + pub tasks: Option, + pub templates: Option>, + pub unknown: Vec>, +} + +#[derive(Debug, Clone)] +pub struct UnknownElement<'a> { + pub signature: [u8; 4], + pub offset: u32, + pub size: u32, + pub data: &'a [u8], +} + +#[derive(Debug, Clone)] +pub struct ChannelDefinitions { + pub offset: u32, + pub size: u32, + pub channels: Vec, +} + +#[derive(Debug, Clone)] +pub struct ChannelDefinition { + pub identifier: u32, + pub name_offset: u32, + pub unknown: u32, + pub message_identifier: Option, + pub name: Option, +} + +#[derive(Debug, Clone)] +pub struct EventDefinitions { + pub offset: u32, + pub size: u32, + pub unknown: u32, + pub events: Vec, + /// Trailing bytes within the EVNT element (currently undocumented). + pub trailing: Vec, +} + +#[derive(Debug, Clone)] +pub struct EventDefinition { + pub identifier: u16, + pub version: u8, + pub channel: u8, + pub level: u8, + pub opcode: u8, + pub task: u16, + pub keywords: u64, + pub message_identifier: u32, + pub template_offset: Option, + pub opcode_offset: Option, + pub level_offset: Option, + pub task_offset: Option, + pub unknown_count: u32, + pub unknown_offset: u32, + pub flags: u32, +} + +/// A stable key for joining provider event metadata to a template definition. +/// +/// This mirrors the fields in the `EVNT` event definition header and is intended to be used +/// alongside `template_offset` → `TEMP` resolution. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct EventKey { + pub provider_guid: String, + pub event_id: u16, + pub version: u8, + pub channel: u8, + pub level: u8, + pub opcode: u8, + pub task: u16, + pub keywords: u64, +} + +#[derive(Debug)] +pub struct CrimManifestIndex<'a> { + /// Template GUID → one or more template definitions (duplicates are unexpected but handled). + pub templates_by_guid: HashMap>>, + /// EventKey → one or more template GUIDs (event definitions can legitimately share templates). + pub event_to_template_guids: HashMap>, +} + +#[derive(Debug, Clone)] +pub struct KeywordDefinitions { + pub offset: u32, + pub size: u32, + pub keywords: Vec, +} + +#[derive(Debug, Clone)] +pub struct KeywordDefinition { + pub identifier: u64, + pub message_identifier: Option, + pub data_offset: u32, + pub name: Option, +} + +#[derive(Debug, Clone)] +pub struct LevelDefinitions { + pub offset: u32, + pub size: u32, + pub levels: Vec, +} + +#[derive(Debug, Clone)] +pub struct LevelDefinition { + pub identifier: u32, + pub message_identifier: Option, + pub data_offset: u32, + pub name: Option, +} + +#[derive(Debug, Clone)] +pub struct OpcodeDefinitions { + pub offset: u32, + pub size: u32, + pub opcodes: Vec, +} + +#[derive(Debug, Clone)] +pub struct OpcodeDefinition { + pub identifier: u32, + pub message_identifier: Option, + pub data_offset: u32, + pub name: Option, +} + +#[derive(Debug, Clone)] +pub struct TaskDefinitions { + pub offset: u32, + pub size: u32, + pub tasks: Vec, +} + +#[derive(Debug, Clone)] +pub struct TaskDefinition { + pub identifier: u32, + pub message_identifier: Option, + pub mui_identifier: Guid, + pub data_offset: u32, + pub name: Option, +} + +#[derive(Debug, Clone)] +pub struct TemplateTable<'a> { + pub offset: u32, + pub size: u32, + pub templates: Vec>, +} + +#[derive(Debug, Clone)] +pub struct TemplateDefinition<'a> { + pub offset: u32, + pub size: u32, + pub item_descriptor_count: u32, + pub item_name_count: u32, + pub template_items_offset: u32, + pub event_type: u32, + pub guid: Guid, + pub binxml: &'a [u8], + pub items: Vec, +} + +#[derive(Debug, Clone)] +pub struct TemplateItem { + pub unknown1: u32, + pub input_type: u8, + pub output_type: u8, + pub unknown3: u16, + pub unknown4: u32, + pub count: u16, + pub length: u16, + pub name_offset: u32, + pub name: Option, +} + +#[derive(Debug, Clone)] +pub struct MapsDefinitions<'a> { + pub offset: u32, + pub size: u32, + pub maps: Vec>, +} + +#[derive(Debug, Clone)] +pub enum MapDefinition<'a> { + ValueMap(ValueMap<'a>), + Bitmap(BitmapMap<'a>), + Unknown { + signature: [u8; 4], + offset: u32, + data: &'a [u8], + }, +} + +#[derive(Debug, Clone)] +pub struct ValueMap<'a> { + pub offset: u32, + pub size: u32, + pub map_string_offset: u32, + pub entries: Vec, + pub map_string: Option, + /// Trailing bytes within this VMAP (if any). + pub trailing: &'a [u8], +} + +#[derive(Debug, Clone)] +pub struct ValueMapEntry { + pub identifier: u32, + pub message_identifier: Option, +} + +#[derive(Debug, Clone)] +pub struct BitmapMap<'a> { + pub offset: u32, + pub data: &'a [u8], +} + +impl Provider<'_> { + /// Resolve a template definition by its offset (as stored in EVNT.template_offset). + pub fn template_by_offset(&self, offset: u32) -> Option<&TemplateDefinition<'_>> { + self.wevt + .elements + .templates + .as_ref() + .and_then(|t| t.templates.iter().find(|tpl| tpl.offset == offset)) + } +} + + diff --git a/src/wevt_templates/manifest/util.rs b/src/wevt_templates/manifest/util.rs new file mode 100644 index 00000000..8ab14636 --- /dev/null +++ b/src/wevt_templates/manifest/util.rs @@ -0,0 +1,140 @@ +use winstructs::guid::Guid; + +use super::error::{Result, WevtManifestError}; +use crate::utils::bytes; + +pub(super) fn read_sized_utf16_string( + buf: &[u8], + offset: u32, + what: &'static str, +) -> Result { + let off_usize = u32_to_usize(offset, what, buf.len())?; + require_len(buf, off_usize, 4, what)?; + let size = read_u32_named(buf, off_usize, what)?; + if size < 4 { + return Err(WevtManifestError::SizeOutOfBounds { what, offset, size }); + } + let size_usize = usize::try_from(size).map_err(|_| WevtManifestError::SizeOutOfBounds { + what, + offset, + size, + })?; + require_len(buf, off_usize, size_usize, what)?; + let bytes = &buf[off_usize + 4..off_usize + size_usize]; + decode_utf16_z(bytes, what, offset) +} + +pub(super) fn decode_utf16_z(bytes: &[u8], what: &'static str, offset: u32) -> Result { + crate::utils::decode_utf16le_bytes_z(bytes) + .map_err(|_| WevtManifestError::InvalidUtf16String { what, offset }) +} + +pub(super) fn read_sig_named(buf: &[u8], offset: usize, what: &'static str) -> Result<[u8; 4]> { + bytes::read_sig(buf, offset).ok_or(WevtManifestError::Truncated { + what, + offset: usize_to_u32(offset), + need: 4, + have: buf.len().saturating_sub(offset), + }) +} + +pub(super) fn read_u8_named(buf: &[u8], offset: usize, what: &'static str) -> Result { + bytes::read_u8(buf, offset).ok_or(WevtManifestError::Truncated { + what, + offset: usize_to_u32(offset), + need: 1, + have: buf.len().saturating_sub(offset), + }) +} + +pub(super) fn read_u16_named(buf: &[u8], offset: usize, what: &'static str) -> Result { + bytes::read_u16_le(buf, offset).ok_or(WevtManifestError::Truncated { + what, + offset: usize_to_u32(offset), + need: 2, + have: buf.len().saturating_sub(offset), + }) +} + +pub(super) fn read_u32_named(buf: &[u8], offset: usize, what: &'static str) -> Result { + bytes::read_u32_le(buf, offset).ok_or(WevtManifestError::Truncated { + what, + offset: usize_to_u32(offset), + need: 4, + have: buf.len().saturating_sub(offset), + }) +} + +pub(super) fn read_u64_named(buf: &[u8], offset: usize, what: &'static str) -> Result { + bytes::read_u64_le(buf, offset).ok_or(WevtManifestError::Truncated { + what, + offset: usize_to_u32(offset), + need: 8, + have: buf.len().saturating_sub(offset), + }) +} + +pub(super) fn read_guid_named(buf: &[u8], offset: usize, what: &'static str) -> Result { + let bytes = bytes::read_array::<16>(buf, offset).ok_or(WevtManifestError::Truncated { + what, + offset: usize_to_u32(offset), + need: 16, + have: buf.len().saturating_sub(offset), + })?; + Guid::from_buffer(&bytes).map_err(|_| WevtManifestError::InvalidGuid { + what, + offset: usize_to_u32(offset), + }) +} + +pub(super) fn u32_to_usize(offset: u32, what: &'static str, len: usize) -> Result { + let off = usize::try_from(offset).map_err(|_| WevtManifestError::OffsetOutOfBounds { + what, + offset, + len, + })?; + if off > len { + return Err(WevtManifestError::OffsetOutOfBounds { what, offset, len }); + } + Ok(off) +} + +pub(super) fn usize_to_u32(v: usize) -> u32 { + u32::try_from(v).unwrap_or(u32::MAX) +} + +pub(super) fn require_len(buf: &[u8], off: usize, need: usize, what: &'static str) -> Result<()> { + if off > buf.len() || buf.len().saturating_sub(off) < need { + return Err(WevtManifestError::Truncated { + what, + offset: usize_to_u32(off), + need, + have: buf.len().saturating_sub(off), + }); + } + Ok(()) +} + +pub(super) fn checked_end(len: usize, off: u32, size: u32, what: &'static str) -> Result { + let off_usize = u32_to_usize(off, what, len)?; + let size_usize = usize::try_from(size).map_err(|_| WevtManifestError::SizeOutOfBounds { + what, + offset: off, + size, + })?; + let end = off_usize + .checked_add(size_usize) + .ok_or(WevtManifestError::SizeOutOfBounds { + what, + offset: off, + size, + })?; + if end > len { + return Err(WevtManifestError::SizeOutOfBounds { + what, + offset: off, + size, + }); + } + Ok(end) +} diff --git a/src/wevt_templates/mod.rs b/src/wevt_templates/mod.rs new file mode 100644 index 00000000..b5a25407 --- /dev/null +++ b/src/wevt_templates/mod.rs @@ -0,0 +1,41 @@ +//! Offline extraction/parsing/rendering of Windows Event Log templates (`WEVT_TEMPLATE`). +//! +//! EVTX records often contain *template instances* (substitution values), while the corresponding +//! *template definitions* are stored in provider PE resources under the `WEVT_TEMPLATE` type. +//! This module provides the pieces needed to build an offline cache and render events without +//! calling Windows APIs. +//! +//! The implementation is split into a few focused submodules: +//! - `extract`: minimal, bounds-checked PE/RSRC parsing to extract `WEVT_TEMPLATE` blobs +//! - `manifest`: spec-backed parsing of the CRIM/WEVT payload, plus stable join keys +//! - `binxml` + `render`: decoding/rendering of the WEVT “inline-name” BinXML dialect +//! - `temp`: helpers for enumerating `TTBL`/`TEMP` entries within a blob (useful for indexing) +//! +//! References: +//! - `docs/wevt_templates.md` (project notes + curated links) +//! - MS-EVEN6 (BinXml inline names + NameHash) +//! - libfwevt manifest format documentation / reference implementation + +pub mod manifest; + +mod binxml; +mod cache; +mod error; +mod extract; +mod record_fallback; +mod render; +mod temp; +mod types; + +pub use binxml::{parse_temp_binxml_fragment, parse_wevt_binxml_fragment}; +pub use cache::{WevtCache, WevtCacheError, normalize_guid}; +pub use error::WevtTemplateExtractError; +pub use extract::extract_wevt_template_resources; +pub use render::{ + render_temp_to_xml, render_temp_to_xml_with_substitution_values, + render_template_definition_to_xml, render_template_definition_to_xml_with_substitution_values, +}; +pub use temp::extract_temp_templates_from_wevt_blob; +pub use types::{ + ResourceIdentifier, WevtTempTemplateHeader, WevtTempTemplateRef, WevtTemplateResource, +}; diff --git a/src/wevt_templates/record_fallback.rs b/src/wevt_templates/record_fallback.rs new file mode 100644 index 00000000..2ae257ee --- /dev/null +++ b/src/wevt_templates/record_fallback.rs @@ -0,0 +1,368 @@ +use crate::SerializedEvtxRecord; + +use crate::model::deserialized::BinXMLDeserializedTokens; + +#[derive(Debug, Clone)] +struct TemplateInstanceInfo { + /// Normalized GUID (lowercased, braces stripped) if we can resolve it. + guid: Option, + substitutions: Vec, +} + +fn extract_template_guid_from_error(err: &crate::err::EvtxError) -> Option { + use crate::err::{DeserializationError, EvtxError}; + match err { + EvtxError::FailedToParseRecord { source, .. } => extract_template_guid_from_error(source), + EvtxError::DeserializationError(DeserializationError::FailedToDeserializeTemplate { + template_id, + .. + }) => Some(template_id.to_string()), + _ => None, + } +} + +fn binxml_value_to_string_lossy(value: &crate::binxml::value_variant::BinXmlValue<'_>) -> String { + use crate::binxml::value_variant::BinXmlValue; + match value { + BinXmlValue::EvtHandle | BinXmlValue::BinXmlType(_) | BinXmlValue::EvtXml => String::new(), + _ => value.as_cow_str().into_owned(), + } +} + +fn substitutions_from_template_instance<'a>( + tpl: &crate::model::deserialized::BinXmlTemplateRef<'a>, +) -> Vec { + tpl.substitution_array + .iter() + .map(|t| match t { + BinXMLDeserializedTokens::Value(v) => binxml_value_to_string_lossy(v), + _ => String::new(), + }) + .collect() +} + +/// Resolve the GUID for a record `TemplateInstance` so we can strictly match it against the +/// template GUID from a deserialization error (`FailedToDeserializeTemplate { template_id: GUID }`). +/// +/// Resolution order (deterministic): +/// 1. **Inline**: `tpl.template_guid` when the record embeds a template definition header inline in +/// the `TemplateInstance`. +/// 2. **Cached**: lookup `tpl.template_def_offset` in the chunk `template_table` and read the cached +/// template definition header GUID. +/// 3. **Direct**: read and validate a `TemplateDefinitionHeader` directly from the chunk bytes at +/// `tpl.template_def_offset` (bounds + plausible header + BinXML fragment header). +/// +/// If none of the above succeeds, returns `None`. In that case WEVT-cache rendering will not be +/// attempted because we cannot prove which substitution array matches the error GUID. +fn resolve_template_guid_from_record<'a>( + record: &crate::EvtxRecord<'a>, + tpl: &crate::model::deserialized::BinXmlTemplateRef<'a>, +) -> Option { + if let Some(g) = tpl.template_guid.as_ref() { + return Some(g.to_string()); + } + + // Prefer the fully-parsed/cached template table (fast path). + if let Some(def) = record + .chunk + .template_table + .get_template(tpl.template_def_offset) + { + return Some(def.header.guid.to_string()); + } + + // Finally: validate and read the template definition header directly from the chunk bytes at + // `template_def_offset`. + crate::binxml::tokens::try_read_template_definition_header_at( + record.chunk.data, + tpl.template_def_offset, + ) + .ok() + .map(|h| h.guid.to_string()) +} + +fn collect_template_instances<'a>(record: &crate::EvtxRecord<'a>) -> Vec { + let mut out = Vec::new(); + + for t in &record.tokens { + let BinXMLDeserializedTokens::TemplateInstance(tpl) = t else { + continue; + }; + + let guid = + resolve_template_guid_from_record(record, tpl).map(|g| super::normalize_guid(&g)); + let substitutions = substitutions_from_template_instance(tpl); + + out.push(TemplateInstanceInfo { + guid, + substitutions, + }); + } + + out +} + +fn select_template_instance_for_guid<'a>( + instances: &'a [TemplateInstanceInfo], + guid: &str, +) -> Option<&'a TemplateInstanceInfo> { + let want = super::normalize_guid(guid); + + let mut matches = instances + .iter() + .filter(|i| i.guid.as_ref().is_some_and(|g| g == &want)); + + let first = matches.next()?; + if matches.next().is_some() { + None + } else { + Some(first) + } +} + +impl crate::EvtxRecord<'_> { + /// Render a record as XML, using the EVTX’s embedded templates first. + /// + /// If rendering fails *specifically because a template definition cannot be deserialized* and + /// the error contains a concrete template GUID, this will deterministically attempt to render + /// the record using the provided offline WEVT cache: + /// - We only use the cache when the error is `FailedToDeserializeTemplate { template_id: GUID }`. + /// - We only proceed when we can unambiguously select the matching `TemplateInstance` + /// substitution array for that GUID. + /// - Otherwise we return the original error unchanged. + /// + /// Note: When the cache is used, the returned `data` is the rendered *template XML fragment*, + /// not the full EVTX `` wrapper. + pub fn into_xml_with_wevt_cache( + self, + cache: &super::WevtCache, + ) -> crate::err::Result> { + let record_id = self.event_record_id; + let timestamp = self.timestamp; + let ansi_codec = self.settings.get_ansi_codec(); + + let instances = collect_template_instances(&self); + + match self.into_xml() { + Ok(r) => Ok(r), + Err(e) => { + let Some(guid) = extract_template_guid_from_error(&e) else { + return Err(e); + }; + + let Some(tpl) = select_template_instance_for_guid(&instances, &guid) else { + return Err(e); + }; + let subs = &tpl.substitutions; + + match cache.render_by_template_guid_with_ansi_codec(&guid, subs, ansi_codec) { + Ok(xml_fragment) => { + log::info!( + "wevt-cache used: record_id={} template_guid={}", + record_id, + guid + ); + Ok(SerializedEvtxRecord { + event_record_id: record_id, + timestamp, + data: xml_fragment, + }) + } + Err(render_err) => { + log::warn!( + "wevt-cache render failed for record {} template_guid={}: {render_err}", + record_id, + guid + ); + Err(e) + } + } + } + } + } + + /// Render a record as JSON, using the EVTX’s embedded templates first. + /// + /// This follows the same deterministic WEVT-cache rule as `into_xml_with_wevt_cache`: + /// only on an explicit template-GUID deserialization failure and only with an unambiguous + /// `TemplateInstance` substitution array. + /// + /// When the cache is used, the JSON output is a synthetic object that contains the rendered XML + /// fragment under `xml` (and includes metadata fields like `template_guid` and `record_id`). + pub fn into_json_with_wevt_cache( + self, + cache: &super::WevtCache, + ) -> crate::err::Result> { + let record_id = self.event_record_id; + let timestamp = self.timestamp; + let indent = self.settings.should_indent(); + let ansi_codec = self.settings.get_ansi_codec(); + + let instances = collect_template_instances(&self); + + match self.into_json() { + Ok(r) => Ok(r), + Err(e) => { + let Some(guid) = extract_template_guid_from_error(&e) else { + return Err(e); + }; + let Some(tpl) = select_template_instance_for_guid(&instances, &guid) else { + return Err(e); + }; + let subs = &tpl.substitutions; + + match cache.render_by_template_guid_with_ansi_codec(&guid, subs, ansi_codec) { + Ok(xml_fragment) => { + log::info!( + "wevt-cache used: record_id={} template_guid={}", + record_id, + guid + ); + let v = serde_json::json!({ + "_wevt_cache_used": true, + "template_guid": guid, + "record_id": record_id, + "timestamp": timestamp.to_rfc3339(), + "xml": xml_fragment, + }); + + let data = if indent { + serde_json::to_string_pretty(&v) + .map_err(crate::err::SerializationError::from)? + } else { + serde_json::to_string(&v) + .map_err(crate::err::SerializationError::from)? + }; + + Ok(SerializedEvtxRecord { + event_record_id: record_id, + timestamp, + data, + }) + } + Err(render_err) => { + log::warn!( + "wevt-cache render failed for record {} template_guid={}: {render_err}", + record_id, + guid + ); + Err(e) + } + } + } + } + } + + /// Like `into_json_with_wevt_cache`, but the "normal path" uses `into_json_stream()` instead of + /// building a full `serde_json::Value` per record. + pub fn into_json_stream_with_wevt_cache( + self, + cache: &super::WevtCache, + ) -> crate::err::Result> { + let record_id = self.event_record_id; + let timestamp = self.timestamp; + let indent = self.settings.should_indent(); + let ansi_codec = self.settings.get_ansi_codec(); + + let instances = collect_template_instances(&self); + + match self.into_json_stream() { + Ok(r) => Ok(r), + Err(e) => { + let Some(guid) = extract_template_guid_from_error(&e) else { + return Err(e); + }; + let Some(tpl) = select_template_instance_for_guid(&instances, &guid) else { + return Err(e); + }; + let subs = &tpl.substitutions; + + match cache.render_by_template_guid_with_ansi_codec(&guid, subs, ansi_codec) { + Ok(xml_fragment) => { + log::info!( + "wevt-cache used: record_id={} template_guid={}", + record_id, + guid + ); + let v = serde_json::json!({ + "_wevt_cache_used": true, + "template_guid": guid, + "record_id": record_id, + "timestamp": timestamp.to_rfc3339(), + "xml": xml_fragment, + }); + + let data = if indent { + serde_json::to_string_pretty(&v) + .map_err(crate::err::SerializationError::from)? + } else { + serde_json::to_string(&v) + .map_err(crate::err::SerializationError::from)? + }; + + Ok(SerializedEvtxRecord { + event_record_id: record_id, + timestamp, + data, + }) + } + Err(render_err) => { + log::warn!( + "wevt-cache render failed for record {} template_guid={}: {render_err}", + record_id, + guid + ); + Err(e) + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn select_template_instance_for_guid_requires_match_even_when_single_instance() { + let want = "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee}"; + let other = "{11111111-2222-3333-4444-555555555555}"; + + let instances = vec![TemplateInstanceInfo { + guid: Some(super::super::normalize_guid(want)), + substitutions: vec![], + }]; + + assert!( + select_template_instance_for_guid(&instances, other).is_none(), + "single instance must not be selected when GUID mismatches" + ); + assert!( + select_template_instance_for_guid(&instances, want).is_some(), + "single instance should be selected when GUID matches" + ); + } + + #[test] + fn select_template_instance_for_guid_requires_unique_match() { + let want = "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee}"; + let g = super::super::normalize_guid(want); + + let instances = vec![ + TemplateInstanceInfo { + guid: Some(g.clone()), + substitutions: vec!["a".to_string()], + }, + TemplateInstanceInfo { + guid: Some(g), + substitutions: vec!["b".to_string()], + }, + ]; + + assert!( + select_template_instance_for_guid(&instances, want).is_none(), + "ambiguous GUID match must be rejected" + ); + } +} diff --git a/src/wevt_templates/render.rs b/src/wevt_templates/render.rs new file mode 100644 index 00000000..5c886f4f --- /dev/null +++ b/src/wevt_templates/render.rs @@ -0,0 +1,818 @@ +//! Rendering helpers for template BinXML. +//! +//! Offline template caching needs deterministic, human-readable XML output. These helpers bridge +//! from the WEVT inline-name BinXML token stream to XML strings, either as a template skeleton +//! (with `{sub:N}` placeholders) or as a fully rendered fragment once substitution values are +//! available. +//! +//! References: +//! - `docs/wevt_templates.md` (project notes + curated links) +//! - MS-EVEN6 (BinXml token grammar + inline names) + +use encoding::EncodingRef; + +use super::binxml::{TEMP_BINXML_OFFSET, parse_temp_binxml_fragment, parse_wevt_binxml_fragment}; + +/// Render a `TEMP` entry to an XML string (with `{sub:N}` placeholders for substitutions). +/// +/// `TEMP` is the raw template definition as shipped in provider resources. Rendering it as an +/// XML *skeleton* is useful for building caches and for debugging, even when you don’t have an +/// EVTX record’s substitution values. +pub fn render_temp_to_xml( + temp_bytes: &[u8], + ansi_codec: EncodingRef, +) -> crate::err::Result { + use crate::ParserSettings; + use crate::binxml::name::read_wevt_inline_name_at; + use crate::binxml::value_variant::BinXmlValue; + use crate::err::{EvtxError, Result}; + use crate::model::xml::{XmlElement, XmlElementBuilder, XmlModel, XmlPIBuilder}; + use crate::xml_output::{BinXmlOutput, XmlOutput}; + use std::borrow::Cow; + + if temp_bytes.len() < TEMP_BINXML_OFFSET { + return Err(EvtxError::calculation_error(format!( + "TEMP too small to contain BinXML fragment header (len={}, need >= {})", + temp_bytes.len(), + TEMP_BINXML_OFFSET + ))); + } + + let binxml = &temp_bytes[TEMP_BINXML_OFFSET..]; + let (tokens, _bytes_consumed) = parse_temp_binxml_fragment(temp_bytes, ansi_codec)?; + + fn resolve_name<'a>( + binxml: &'a [u8], + name_ref: &crate::binxml::name::BinXmlNameRef, + ) -> Result> { + Ok(Cow::Owned(read_wevt_inline_name_at( + binxml, + name_ref.offset, + )?)) + } + + // Build a record model similar to `binxml::assemble::create_record_model`, + // but resolving names via WEVT inline-name decoding and allowing substitution placeholders. + let mut current_element: Option = None; + let mut current_pi: Option = None; + let mut model: Vec = Vec::with_capacity(tokens.len()); + + for token in tokens { + match token { + crate::model::deserialized::BinXMLDeserializedTokens::FragmentHeader(_) => {} + crate::model::deserialized::BinXMLDeserializedTokens::TemplateInstance(_) => { + return Err(EvtxError::Unimplemented { + name: "TemplateInstance inside WEVT TEMP BinXML".to_string(), + }); + } + crate::model::deserialized::BinXMLDeserializedTokens::AttributeList => {} + crate::model::deserialized::BinXMLDeserializedTokens::CloseElement => { + model.push(XmlModel::CloseElement); + } + crate::model::deserialized::BinXMLDeserializedTokens::CloseStartElement => { + match current_element.take() { + None => { + return Err(EvtxError::FailedToCreateRecordModel( + "close start - Bad parser state", + )); + } + Some(builder) => model.push(XmlModel::OpenElement(builder.finish()?)), + }; + } + crate::model::deserialized::BinXMLDeserializedTokens::CDATASection => { + return Err(EvtxError::FailedToCreateRecordModel( + "Unimplemented - CDATA", + )); + } + crate::model::deserialized::BinXMLDeserializedTokens::CharRef => { + return Err(EvtxError::FailedToCreateRecordModel( + "Unimplemented - CharacterReference", + )); + } + crate::model::deserialized::BinXMLDeserializedTokens::EntityRef(ref entity) => { + model.push(XmlModel::EntityRef(resolve_name(binxml, &entity.name)?)) + } + crate::model::deserialized::BinXMLDeserializedTokens::PITarget(ref name) => { + let mut builder = XmlPIBuilder::new(); + builder.name(resolve_name(binxml, &name.name)?); + current_pi = Some(builder); + } + crate::model::deserialized::BinXMLDeserializedTokens::PIData(data) => { + match current_pi.take() { + None => { + return Err(EvtxError::FailedToCreateRecordModel( + "PI Data without PI target - Bad parser state", + )); + } + Some(mut builder) => { + builder.data(Cow::Owned(data)); + model.push(builder.finish()); + } + } + } + crate::model::deserialized::BinXMLDeserializedTokens::Substitution(sub) => { + let placeholder = format!("{{sub:{}}}", sub.substitution_index); + let value = BinXmlValue::StringType(placeholder); + match current_element { + None => model.push(XmlModel::Value(Cow::Owned(value))), + Some(ref mut builder) => { + builder.attribute_value(Cow::Owned(value))?; + } + } + } + crate::model::deserialized::BinXMLDeserializedTokens::EndOfStream => { + model.push(XmlModel::EndOfStream) + } + crate::model::deserialized::BinXMLDeserializedTokens::StartOfStream => { + model.push(XmlModel::StartOfStream) + } + crate::model::deserialized::BinXMLDeserializedTokens::CloseEmptyElement => { + match current_element.take() { + None => { + return Err(EvtxError::FailedToCreateRecordModel( + "close empty - Bad parser state", + )); + } + Some(builder) => { + model.push(XmlModel::OpenElement(builder.finish()?)); + model.push(XmlModel::CloseElement); + } + }; + } + crate::model::deserialized::BinXMLDeserializedTokens::Attribute(ref attr) => { + if current_element.is_none() { + return Err(EvtxError::FailedToCreateRecordModel( + "attribute - Bad parser state", + )); + } + if let Some(builder) = current_element.as_mut() { + builder.attribute_name(resolve_name(binxml, &attr.name)?) + } + } + crate::model::deserialized::BinXMLDeserializedTokens::OpenStartElement(ref elem) => { + let mut builder = XmlElementBuilder::new(); + builder.name(resolve_name(binxml, &elem.name)?); + current_element = Some(builder); + } + crate::model::deserialized::BinXMLDeserializedTokens::Value(value) => { + match current_element { + None => match value { + BinXmlValue::EvtXml => { + return Err(EvtxError::FailedToCreateRecordModel( + "Unexpected EvtXml in WEVT TEMP BinXML", + )); + } + _ => { + model.push(XmlModel::Value(Cow::Owned(value))); + } + }, + Some(ref mut builder) => { + builder.attribute_value(Cow::Owned(value))?; + } + } + } + } + } + + let settings = ParserSettings::default().ansi_codec(ansi_codec); + let mut output = XmlOutput::with_writer(Vec::new(), &settings); + + output.visit_start_of_stream()?; + let mut stack: Vec = Vec::new(); + + for owned_token in model { + match owned_token { + XmlModel::OpenElement(open_element) => { + stack.push(open_element); + output.visit_open_start_element(stack.last().ok_or({ + EvtxError::FailedToCreateRecordModel( + "Invalid parser state - expected stack to be non-empty", + ) + })?)?; + } + XmlModel::CloseElement => { + let close_element = stack.pop().ok_or({ + EvtxError::FailedToCreateRecordModel( + "Invalid parser state - expected stack to be non-empty", + ) + })?; + output.visit_close_element(&close_element)? + } + XmlModel::Value(s) => output.visit_characters(s)?, + XmlModel::EndOfStream => {} + XmlModel::StartOfStream => {} + XmlModel::PI(pi) => output.visit_processing_instruction(&pi)?, + XmlModel::EntityRef(entity) => output.visit_entity_reference(&entity)?, + }; + } + + output.visit_end_of_stream()?; + + String::from_utf8(output.into_writer()).map_err(|e| EvtxError::calculation_error(e.to_string())) +} + +/// Render a `TEMP` entry to an XML string, applying substitution values. +/// +/// This is the "last mile" helper for offline rendering: given a raw `TEMP` blob (as extracted +/// from `WEVT_TEMPLATE`) and the corresponding substitution values array from an EVTX record's +/// `TemplateInstance`, emit the fully rendered XML fragment. +/// +/// Substitution values are provided as strings and will be inserted as text/attribute values. +/// XML escaping is handled by `XmlOutput`. +pub fn render_temp_to_xml_with_substitution_values( + temp_bytes: &[u8], + substitution_values: &[String], + ansi_codec: EncodingRef, +) -> crate::err::Result { + use crate::ParserSettings; + use crate::binxml::name::read_wevt_inline_name_at; + use crate::binxml::value_variant::BinXmlValue; + use crate::err::{EvtxError, Result}; + use crate::model::xml::{XmlElement, XmlElementBuilder, XmlModel, XmlPIBuilder}; + use crate::xml_output::{BinXmlOutput, XmlOutput}; + use std::borrow::Cow; + + if temp_bytes.len() < TEMP_BINXML_OFFSET { + return Err(EvtxError::calculation_error(format!( + "TEMP too small to contain BinXML fragment header (len={}, need >= {})", + temp_bytes.len(), + TEMP_BINXML_OFFSET + ))); + } + + let binxml = &temp_bytes[TEMP_BINXML_OFFSET..]; + let (tokens, _bytes_consumed) = parse_temp_binxml_fragment(temp_bytes, ansi_codec)?; + + fn resolve_name<'a>( + binxml: &'a [u8], + name_ref: &crate::binxml::name::BinXmlNameRef, + ) -> Result> { + Ok(Cow::Owned(read_wevt_inline_name_at( + binxml, + name_ref.offset, + )?)) + } + + let mut current_element: Option = None; + let mut current_pi: Option = None; + let mut model: Vec = Vec::with_capacity(tokens.len()); + + for token in tokens { + match token { + crate::model::deserialized::BinXMLDeserializedTokens::FragmentHeader(_) => {} + crate::model::deserialized::BinXMLDeserializedTokens::TemplateInstance(_) => { + return Err(EvtxError::Unimplemented { + name: "TemplateInstance inside WEVT TEMP BinXML".to_string(), + }); + } + crate::model::deserialized::BinXMLDeserializedTokens::AttributeList => {} + crate::model::deserialized::BinXMLDeserializedTokens::CloseElement => { + model.push(XmlModel::CloseElement); + } + crate::model::deserialized::BinXMLDeserializedTokens::CloseStartElement => { + match current_element.take() { + None => { + return Err(EvtxError::FailedToCreateRecordModel( + "close start - Bad parser state", + )); + } + Some(builder) => model.push(XmlModel::OpenElement(builder.finish()?)), + }; + } + crate::model::deserialized::BinXMLDeserializedTokens::CDATASection => { + return Err(EvtxError::FailedToCreateRecordModel( + "Unimplemented - CDATA", + )); + } + crate::model::deserialized::BinXMLDeserializedTokens::CharRef => { + return Err(EvtxError::FailedToCreateRecordModel( + "Unimplemented - CharacterReference", + )); + } + crate::model::deserialized::BinXMLDeserializedTokens::EntityRef(ref entity) => { + model.push(XmlModel::EntityRef(resolve_name(binxml, &entity.name)?)) + } + crate::model::deserialized::BinXMLDeserializedTokens::PITarget(ref name) => { + let mut builder = XmlPIBuilder::new(); + builder.name(resolve_name(binxml, &name.name)?); + current_pi = Some(builder); + } + crate::model::deserialized::BinXMLDeserializedTokens::PIData(data) => { + match current_pi.take() { + None => { + return Err(EvtxError::FailedToCreateRecordModel( + "PI Data without PI target - Bad parser state", + )); + } + Some(mut builder) => { + builder.data(Cow::Owned(data)); + model.push(builder.finish()); + } + } + } + crate::model::deserialized::BinXMLDeserializedTokens::Substitution(sub) => { + if sub.ignore { + continue; + } + let idx = sub.substitution_index as usize; + let s = substitution_values.get(idx).cloned().unwrap_or_default(); + let value = BinXmlValue::StringType(s); + + match current_element { + None => model.push(XmlModel::Value(Cow::Owned(value))), + Some(ref mut builder) => { + builder.attribute_value(Cow::Owned(value))?; + } + } + } + crate::model::deserialized::BinXMLDeserializedTokens::EndOfStream => { + model.push(XmlModel::EndOfStream) + } + crate::model::deserialized::BinXMLDeserializedTokens::StartOfStream => { + model.push(XmlModel::StartOfStream) + } + crate::model::deserialized::BinXMLDeserializedTokens::CloseEmptyElement => { + match current_element.take() { + None => { + return Err(EvtxError::FailedToCreateRecordModel( + "close empty - Bad parser state", + )); + } + Some(builder) => { + model.push(XmlModel::OpenElement(builder.finish()?)); + model.push(XmlModel::CloseElement); + } + }; + } + crate::model::deserialized::BinXMLDeserializedTokens::Attribute(ref attr) => { + if current_element.is_none() { + return Err(EvtxError::FailedToCreateRecordModel( + "attribute - Bad parser state", + )); + } + if let Some(builder) = current_element.as_mut() { + builder.attribute_name(resolve_name(binxml, &attr.name)?) + } + } + crate::model::deserialized::BinXMLDeserializedTokens::OpenStartElement(ref elem) => { + let mut builder = XmlElementBuilder::new(); + builder.name(resolve_name(binxml, &elem.name)?); + current_element = Some(builder); + } + crate::model::deserialized::BinXMLDeserializedTokens::Value(value) => { + match current_element { + None => match value { + BinXmlValue::EvtXml => { + return Err(EvtxError::FailedToCreateRecordModel( + "Unexpected EvtXml in WEVT TEMP BinXML", + )); + } + _ => { + model.push(XmlModel::Value(Cow::Owned(value))); + } + }, + Some(ref mut builder) => { + builder.attribute_value(Cow::Owned(value))?; + } + } + } + } + } + + let settings = ParserSettings::default().ansi_codec(ansi_codec); + let mut output = XmlOutput::with_writer(Vec::new(), &settings); + + output.visit_start_of_stream()?; + let mut stack: Vec = Vec::new(); + + for owned_token in model { + match owned_token { + XmlModel::OpenElement(open_element) => { + stack.push(open_element); + output.visit_open_start_element(stack.last().ok_or({ + EvtxError::FailedToCreateRecordModel( + "Invalid parser state - expected stack to be non-empty", + ) + })?)?; + } + XmlModel::CloseElement => { + let close_element = stack.pop().ok_or({ + EvtxError::FailedToCreateRecordModel( + "Invalid parser state - expected stack to be non-empty", + ) + })?; + output.visit_close_element(&close_element)? + } + XmlModel::Value(s) => output.visit_characters(s)?, + XmlModel::EndOfStream => {} + XmlModel::StartOfStream => {} + XmlModel::PI(pi) => output.visit_processing_instruction(&pi)?, + XmlModel::EntityRef(entity) => output.visit_entity_reference(&entity)?, + }; + } + + output.visit_end_of_stream()?; + + String::from_utf8(output.into_writer()) + .map_err(|e| EvtxError::calculation_error(e.to_string())) +} + +/// Render a parsed template definition to XML. +/// +/// Compared to `render_temp_to_xml`, this variant can annotate substitutions using the parsed +/// template item descriptors/names (from the CRIM blob). +/// +/// Caches and diagnostics benefit from stable, readable placeholders (`{sub:idx:name}`) instead +/// of only positional ones. +pub fn render_template_definition_to_xml( + template: &crate::wevt_templates::manifest::TemplateDefinition<'_>, + ansi_codec: EncodingRef, +) -> crate::err::Result { + use crate::ParserSettings; + use crate::binxml::name::read_wevt_inline_name_at; + use crate::binxml::value_variant::BinXmlValue; + use crate::err::{EvtxError, Result}; + use crate::model::xml::{XmlElement, XmlElementBuilder, XmlModel, XmlPIBuilder}; + use crate::xml_output::{BinXmlOutput, XmlOutput}; + use std::borrow::Cow; + + let binxml = template.binxml; + let (tokens, _bytes_consumed) = parse_wevt_binxml_fragment(binxml, ansi_codec)?; + + fn resolve_name<'a>( + binxml: &'a [u8], + name_ref: &crate::binxml::name::BinXmlNameRef, + ) -> Result> { + Ok(Cow::Owned(read_wevt_inline_name_at( + binxml, + name_ref.offset, + )?)) + } + + let mut current_element: Option = None; + let mut current_pi: Option = None; + let mut model: Vec = Vec::with_capacity(tokens.len()); + + for token in tokens { + match token { + crate::model::deserialized::BinXMLDeserializedTokens::FragmentHeader(_) => {} + crate::model::deserialized::BinXMLDeserializedTokens::TemplateInstance(_) => { + return Err(EvtxError::Unimplemented { + name: "TemplateInstance inside WEVT TEMP BinXML".to_string(), + }); + } + crate::model::deserialized::BinXMLDeserializedTokens::AttributeList => {} + crate::model::deserialized::BinXMLDeserializedTokens::CloseElement => { + model.push(XmlModel::CloseElement); + } + crate::model::deserialized::BinXMLDeserializedTokens::CloseStartElement => { + match current_element.take() { + None => { + return Err(EvtxError::FailedToCreateRecordModel( + "close start - Bad parser state", + )); + } + Some(builder) => model.push(XmlModel::OpenElement(builder.finish()?)), + }; + } + crate::model::deserialized::BinXMLDeserializedTokens::CDATASection => { + return Err(EvtxError::FailedToCreateRecordModel( + "Unimplemented - CDATA", + )); + } + crate::model::deserialized::BinXMLDeserializedTokens::CharRef => { + return Err(EvtxError::FailedToCreateRecordModel( + "Unimplemented - CharacterReference", + )); + } + crate::model::deserialized::BinXMLDeserializedTokens::EntityRef(ref entity) => { + model.push(XmlModel::EntityRef(resolve_name(binxml, &entity.name)?)) + } + crate::model::deserialized::BinXMLDeserializedTokens::PITarget(ref name) => { + let mut builder = XmlPIBuilder::new(); + builder.name(resolve_name(binxml, &name.name)?); + current_pi = Some(builder); + } + crate::model::deserialized::BinXMLDeserializedTokens::PIData(data) => { + match current_pi.take() { + None => { + return Err(EvtxError::FailedToCreateRecordModel( + "PI Data without PI target - Bad parser state", + )); + } + Some(mut builder) => { + builder.data(Cow::Owned(data)); + model.push(builder.finish()); + } + } + } + crate::model::deserialized::BinXMLDeserializedTokens::Substitution(sub) => { + let idx = sub.substitution_index as usize; + let mut placeholder = format!("{{sub:{idx}}}"); + + if let Some(name) = template + .items + .get(idx) + .and_then(|item| item.name.as_deref()) + { + placeholder = format!("{{sub:{idx}:{name}}}"); + } + + let value = BinXmlValue::StringType(placeholder); + match current_element { + None => model.push(XmlModel::Value(Cow::Owned(value))), + Some(ref mut builder) => { + builder.attribute_value(Cow::Owned(value))?; + } + } + } + crate::model::deserialized::BinXMLDeserializedTokens::EndOfStream => { + model.push(XmlModel::EndOfStream) + } + crate::model::deserialized::BinXMLDeserializedTokens::StartOfStream => { + model.push(XmlModel::StartOfStream) + } + crate::model::deserialized::BinXMLDeserializedTokens::CloseEmptyElement => { + match current_element.take() { + None => { + return Err(EvtxError::FailedToCreateRecordModel( + "close empty - Bad parser state", + )); + } + Some(builder) => { + model.push(XmlModel::OpenElement(builder.finish()?)); + model.push(XmlModel::CloseElement); + } + }; + } + crate::model::deserialized::BinXMLDeserializedTokens::Attribute(ref attr) => { + if current_element.is_none() { + return Err(EvtxError::FailedToCreateRecordModel( + "attribute - Bad parser state", + )); + } + if let Some(builder) = current_element.as_mut() { + builder.attribute_name(resolve_name(binxml, &attr.name)?) + } + } + crate::model::deserialized::BinXMLDeserializedTokens::OpenStartElement(ref elem) => { + let mut builder = XmlElementBuilder::new(); + builder.name(resolve_name(binxml, &elem.name)?); + current_element = Some(builder); + } + crate::model::deserialized::BinXMLDeserializedTokens::Value(value) => { + match current_element { + None => match value { + BinXmlValue::EvtXml => { + return Err(EvtxError::FailedToCreateRecordModel( + "Unexpected EvtXml in WEVT TEMP BinXML", + )); + } + _ => { + model.push(XmlModel::Value(Cow::Owned(value))); + } + }, + Some(ref mut builder) => { + builder.attribute_value(Cow::Owned(value))?; + } + } + } + } + } + + let settings = ParserSettings::default().ansi_codec(ansi_codec); + let mut output = XmlOutput::with_writer(Vec::new(), &settings); + + output.visit_start_of_stream()?; + let mut stack: Vec = Vec::new(); + + for owned_token in model { + match owned_token { + XmlModel::OpenElement(open_element) => { + stack.push(open_element); + output.visit_open_start_element(stack.last().ok_or({ + EvtxError::FailedToCreateRecordModel( + "Invalid parser state - expected stack to be non-empty", + ) + })?)?; + } + XmlModel::CloseElement => { + let close_element = stack.pop().ok_or({ + EvtxError::FailedToCreateRecordModel( + "Invalid parser state - expected stack to be non-empty", + ) + })?; + output.visit_close_element(&close_element)? + } + XmlModel::Value(s) => output.visit_characters(s)?, + XmlModel::EndOfStream => {} + XmlModel::StartOfStream => {} + XmlModel::PI(pi) => output.visit_processing_instruction(&pi)?, + XmlModel::EntityRef(entity) => output.visit_entity_reference(&entity)?, + }; + } + + output.visit_end_of_stream()?; + + String::from_utf8(output.into_writer()).map_err(|e| EvtxError::calculation_error(e.to_string())) +} + +/// Render a parsed template definition to XML, applying substitution values. +/// +/// This is the "last mile" for offline rendering: given a template definition (from `WEVT_TEMPLATE`) +/// and the corresponding substitution values array (from an EVTX record's `TemplateInstance`), +/// emit a fully-rendered XML event fragment. +/// +/// The `substitution_values` are provided as strings and are inserted as text/attribute values. +/// XML escaping is handled by `XmlOutput`. +/// +/// Once an offline cache provides the template definition, this function lets tooling render +/// records end-to-end without access to the original provider binaries. +pub fn render_template_definition_to_xml_with_substitution_values( + template: &crate::wevt_templates::manifest::TemplateDefinition<'_>, + substitution_values: &[String], + ansi_codec: EncodingRef, +) -> crate::err::Result { + use crate::ParserSettings; + use crate::binxml::name::read_wevt_inline_name_at; + use crate::binxml::value_variant::BinXmlValue; + use crate::err::{EvtxError, Result}; + use crate::model::xml::{XmlElement, XmlElementBuilder, XmlModel, XmlPIBuilder}; + use crate::xml_output::{BinXmlOutput, XmlOutput}; + use std::borrow::Cow; + + let binxml = template.binxml; + let (tokens, _bytes_consumed) = parse_wevt_binxml_fragment(binxml, ansi_codec)?; + + fn resolve_name<'a>( + binxml: &'a [u8], + name_ref: &crate::binxml::name::BinXmlNameRef, + ) -> Result> { + Ok(Cow::Owned(read_wevt_inline_name_at( + binxml, + name_ref.offset, + )?)) + } + + let mut current_element: Option = None; + let mut current_pi: Option = None; + let mut model: Vec = Vec::with_capacity(tokens.len()); + + for token in tokens { + match token { + crate::model::deserialized::BinXMLDeserializedTokens::FragmentHeader(_) => {} + crate::model::deserialized::BinXMLDeserializedTokens::TemplateInstance(_) => { + return Err(EvtxError::Unimplemented { + name: "TemplateInstance inside WEVT TEMP BinXML".to_string(), + }); + } + crate::model::deserialized::BinXMLDeserializedTokens::AttributeList => {} + crate::model::deserialized::BinXMLDeserializedTokens::CloseElement => { + model.push(XmlModel::CloseElement); + } + crate::model::deserialized::BinXMLDeserializedTokens::CloseStartElement => { + match current_element.take() { + None => { + return Err(EvtxError::FailedToCreateRecordModel( + "close start - Bad parser state", + )); + } + Some(builder) => model.push(XmlModel::OpenElement(builder.finish()?)), + }; + } + crate::model::deserialized::BinXMLDeserializedTokens::CDATASection => { + return Err(EvtxError::FailedToCreateRecordModel( + "Unimplemented - CDATA", + )); + } + crate::model::deserialized::BinXMLDeserializedTokens::CharRef => { + return Err(EvtxError::FailedToCreateRecordModel( + "Unimplemented - CharacterReference", + )); + } + crate::model::deserialized::BinXMLDeserializedTokens::EntityRef(ref entity) => { + model.push(XmlModel::EntityRef(resolve_name(binxml, &entity.name)?)) + } + crate::model::deserialized::BinXMLDeserializedTokens::PITarget(ref name) => { + let mut builder = XmlPIBuilder::new(); + builder.name(resolve_name(binxml, &name.name)?); + current_pi = Some(builder); + } + crate::model::deserialized::BinXMLDeserializedTokens::PIData(data) => { + match current_pi.take() { + None => { + return Err(EvtxError::FailedToCreateRecordModel( + "PI Data without PI target - Bad parser state", + )); + } + Some(mut builder) => { + builder.data(Cow::Owned(data)); + model.push(builder.finish()); + } + } + } + crate::model::deserialized::BinXMLDeserializedTokens::Substitution(sub) => { + if sub.ignore { + continue; + } + let idx = sub.substitution_index as usize; + let s = substitution_values.get(idx).cloned().unwrap_or_default(); + let value = BinXmlValue::StringType(s); + + match current_element { + None => model.push(XmlModel::Value(Cow::Owned(value))), + Some(ref mut builder) => { + builder.attribute_value(Cow::Owned(value))?; + } + } + } + crate::model::deserialized::BinXMLDeserializedTokens::EndOfStream => { + model.push(XmlModel::EndOfStream) + } + crate::model::deserialized::BinXMLDeserializedTokens::StartOfStream => { + model.push(XmlModel::StartOfStream) + } + crate::model::deserialized::BinXMLDeserializedTokens::CloseEmptyElement => { + match current_element.take() { + None => { + return Err(EvtxError::FailedToCreateRecordModel( + "close empty - Bad parser state", + )); + } + Some(builder) => { + model.push(XmlModel::OpenElement(builder.finish()?)); + model.push(XmlModel::CloseElement); + } + }; + } + crate::model::deserialized::BinXMLDeserializedTokens::Attribute(ref attr) => { + if current_element.is_none() { + return Err(EvtxError::FailedToCreateRecordModel( + "attribute - Bad parser state", + )); + } + if let Some(builder) = current_element.as_mut() { + builder.attribute_name(resolve_name(binxml, &attr.name)?) + } + } + crate::model::deserialized::BinXMLDeserializedTokens::OpenStartElement(ref elem) => { + let mut builder = XmlElementBuilder::new(); + builder.name(resolve_name(binxml, &elem.name)?); + current_element = Some(builder); + } + crate::model::deserialized::BinXMLDeserializedTokens::Value(value) => { + match current_element { + None => match value { + BinXmlValue::EvtXml => { + return Err(EvtxError::FailedToCreateRecordModel( + "Unexpected EvtXml in WEVT TEMP BinXML", + )); + } + _ => { + model.push(XmlModel::Value(Cow::Owned(value))); + } + }, + Some(ref mut builder) => { + builder.attribute_value(Cow::Owned(value))?; + } + } + } + } + } + + let settings = ParserSettings::default().ansi_codec(ansi_codec); + let mut output = XmlOutput::with_writer(Vec::new(), &settings); + + output.visit_start_of_stream()?; + let mut stack: Vec = Vec::new(); + + for owned_token in model { + match owned_token { + XmlModel::OpenElement(open_element) => { + stack.push(open_element); + output.visit_open_start_element(stack.last().ok_or({ + EvtxError::FailedToCreateRecordModel( + "Invalid parser state - expected stack to be non-empty", + ) + })?)?; + } + XmlModel::CloseElement => { + let close_element = stack.pop().ok_or({ + EvtxError::FailedToCreateRecordModel( + "Invalid parser state - expected stack to be non-empty", + ) + })?; + output.visit_close_element(&close_element)? + } + XmlModel::Value(s) => output.visit_characters(s)?, + XmlModel::EndOfStream => {} + XmlModel::StartOfStream => {} + XmlModel::PI(pi) => output.visit_processing_instruction(&pi)?, + XmlModel::EntityRef(entity) => output.visit_entity_reference(&entity)?, + }; + } + + output.visit_end_of_stream()?; + + String::from_utf8(output.into_writer()).map_err(|e| EvtxError::calculation_error(e.to_string())) +} diff --git a/src/wevt_templates/temp.rs b/src/wevt_templates/temp.rs new file mode 100644 index 00000000..66f21470 --- /dev/null +++ b/src/wevt_templates/temp.rs @@ -0,0 +1,45 @@ +//! Utilities for enumerating `TTBL`/`TEMP` entries inside a `WEVT_TEMPLATE` blob. +//! +//! This is mostly used for indexing/debugging: callers can discover all template definitions +//! present in a provider resource blob without re-implementing CRIM/WEVT traversal. +//! +//! References: +//! - `docs/wevt_templates.md` (project notes + curated links) +//! - libfwevt manifest format documentation (CRIM/WEVT/TTBL/TEMP tables) + +use super::types::{WevtTempTemplateHeader, WevtTempTemplateRef}; + +/// Many real-world blobs contain multiple `TTBL` sections. This function finds all parseable +/// `TTBL` sections and returns references to all `TEMP` entries contained within them. +/// +/// This uses the CRIM/WEVT provider element directory to locate `TTBL` elements, and then parses +/// the `TTBL`/`TEMP` structures. +pub fn extract_temp_templates_from_wevt_blob( + blob: &[u8], +) -> Result, super::manifest::WevtManifestError> { + let mut out = Vec::new(); + + let manifest = super::manifest::CrimManifest::parse(blob)?; + + for provider in &manifest.providers { + let Some(ttbl) = provider.wevt.elements.templates.as_ref() else { + continue; + }; + for tpl in &ttbl.templates { + out.push(WevtTempTemplateRef { + ttbl_offset: ttbl.offset, + temp_offset: tpl.offset, + temp_size: tpl.size, + header: WevtTempTemplateHeader { + item_descriptor_count: tpl.item_descriptor_count, + item_name_count: tpl.item_name_count, + template_items_offset: tpl.template_items_offset, + event_type: tpl.event_type, + guid: tpl.guid.clone(), + }, + }); + } + } + + Ok(out) +} diff --git a/src/wevt_templates/types.rs b/src/wevt_templates/types.rs new file mode 100644 index 00000000..b5824a0b --- /dev/null +++ b/src/wevt_templates/types.rs @@ -0,0 +1,51 @@ +use winstructs::guid::Guid; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ResourceIdentifier { + Id(u32), + Name(String), +} + +#[derive(Debug, Clone)] +pub struct WevtTemplateResource { + /// The second-level entry under the `WEVT_TEMPLATE` resource type (often `1`). + pub resource: ResourceIdentifier, + /// Language ID associated with this resource data. + pub lang_id: u32, + /// Raw resource bytes (typically starts with `CRIM|K\0\0`). + pub data: Vec, +} + +// === Parsing of WEVT_TEMPLATE payloads (CRIM/WEVT/TTBL/TEMP) === +// +// Primary references: +// - MS-EVEN6 BinXml grammar (inline names): `Name = NameHash NameNumChars NullTerminatedUnicodeString` +// and token layouts for OpenStartElement/Attribute/EntityRef/PITarget. +// - libfwevt docs: "Windows Event manifest binary format" (WEVT_TEMPLATE / CRIM / WEVT / TTBL / TEMP layouts). + +#[derive(Debug, Clone)] +pub struct WevtTempTemplateHeader { + /// Number of template item descriptors. + pub item_descriptor_count: u32, + /// Number of template item names. + pub item_name_count: u32, + /// Template items offset (relative to the start of the CRIM blob). + pub template_items_offset: u32, + /// Unknown; libfwevt suggests this correlates with the template kind (e.g. EventData vs UserData). + pub event_type: u32, + /// Template GUID. + pub guid: Guid, +} + +#[derive(Debug, Clone)] +pub struct WevtTempTemplateRef { + /// Offset of the containing `TTBL` within the resource blob. + pub ttbl_offset: u32, + /// Offset of this `TEMP` structure within the resource blob. + pub temp_offset: u32, + /// Total size of this `TEMP` structure, in bytes. + pub temp_size: u32, + pub header: WevtTempTemplateHeader, +} + + diff --git a/tests/fixtures.rs b/tests/fixtures.rs index f4fad534..a20f3858 100644 --- a/tests/fixtures.rs +++ b/tests/fixtures.rs @@ -2,9 +2,14 @@ use std::path::PathBuf; use std::sync::Once; +use std::sync::Mutex; static LOGGER_INIT: Once = Once::new(); +// CLI tests can behave flakily when multiple `evtx_dump` subprocesses run concurrently +// (especially when using pty-based interaction). Serialize all CLI-spawning tests. +pub static CLI_TEST_LOCK: Mutex<()> = Mutex::new(()); + // Rust runs the tests concurrently, so unless we synchronize logging access // it will crash when attempting to run `cargo test` with some logging facilities. #[cfg(test)] diff --git a/tests/fixtures/wevt_template_minimal_pe.bin b/tests/fixtures/wevt_template_minimal_pe.bin new file mode 100644 index 00000000..c0dcb564 Binary files /dev/null and b/tests/fixtures/wevt_template_minimal_pe.bin differ diff --git a/tests/test_cli.rs b/tests/test_cli.rs index 8185425f..d84ed45b 100644 --- a/tests/test_cli.rs +++ b/tests/test_cli.rs @@ -11,6 +11,7 @@ use tempfile::tempdir; #[test] fn it_respects_directory_output() { + let _guard = CLI_TEST_LOCK.lock().unwrap(); let d = tempdir().unwrap(); let f = d.as_ref().join("test.out"); @@ -35,6 +36,7 @@ fn it_respects_directory_output() { #[test] fn test_it_refuses_to_overwrite_directory() { + let _guard = CLI_TEST_LOCK.lock().unwrap(); let d = tempdir().unwrap(); let sample = regular_sample(); @@ -46,6 +48,7 @@ fn test_it_refuses_to_overwrite_directory() { #[test] fn test_it_overwrites_file_anyways_if_passed_flag() { + let _guard = CLI_TEST_LOCK.lock().unwrap(); let d = tempdir().unwrap(); let f = d.as_ref().join("test.out"); @@ -74,6 +77,7 @@ fn test_it_overwrites_file_anyways_if_passed_flag() { #[test] fn it_supports_stdin_input_with_dash() { + let _guard = CLI_TEST_LOCK.lock().unwrap(); let sample = regular_sample(); // Pick a single record id to keep CLI output small/deterministic. diff --git a/tests/test_cli_interactive.rs b/tests/test_cli_interactive.rs index 324fc11f..b4d58d4c 100644 --- a/tests/test_cli_interactive.rs +++ b/tests/test_cli_interactive.rs @@ -13,18 +13,31 @@ mod tests { use rexpect::spawn; use std::fs::File; use std::io::{Read, Write}; - use std::sync::Mutex; + use std::time::{Duration, Instant}; use tempfile::tempdir; - // These tests rely on pty semantics and can behave flakily when executed concurrently. - // Serialize them to ensure stable behavior under the default Rust test runner. - static INTERACTIVE_TEST_LOCK: Mutex<()> = Mutex::new(()); + fn wait_for_file_len_at_least(path: &std::path::Path, min_len: usize, timeout: Duration) -> usize { + let start = Instant::now(); + loop { + if let Ok(meta) = std::fs::metadata(path) { + let len = meta.len() as usize; + if len >= min_len { + return len; + } + } + if start.elapsed() >= timeout { + let len = std::fs::metadata(path).map(|m| m.len() as usize).unwrap_or(0); + return len; + } + std::thread::sleep(Duration::from_millis(25)); + } + } // It should behave the same on windows, but interactive testing relies on unix pty internals. #[test] #[cfg(not(target_os = "windows"))] fn test_it_confirms_before_overwriting_a_file() { - let _guard = INTERACTIVE_TEST_LOCK.lock().unwrap(); + let _guard = CLI_TEST_LOCK.lock().unwrap(); let d = tempdir().unwrap(); let f = d.as_ref().join("test.out"); @@ -45,22 +58,16 @@ mod tests { .unwrap(); p.send_line("y").unwrap(); p.flush().unwrap(); - std::thread::sleep(std::time::Duration::from_millis(100)); - // .expect_eof doesn't work any more :( - - let mut expected = vec![]; - File::open(&f).unwrap().read_to_end(&mut expected).unwrap(); - assert!( - expected.len() > 100, - "Expected output to be printed to file" - ) + // Wait for the file to be overwritten. Under load, parsing can take longer than 100ms. + let len = wait_for_file_len_at_least(&f, 100, Duration::from_secs(10)); + assert!(len >= 100, "Expected output to be printed to file"); } #[test] #[cfg(not(target_os = "windows"))] fn test_it_confirms_before_overwriting_a_file_and_quits() { - let _guard = INTERACTIVE_TEST_LOCK.lock().unwrap(); + let _guard = CLI_TEST_LOCK.lock().unwrap(); let d = tempdir().unwrap(); let f = d.as_ref().join("test.out"); @@ -80,14 +87,16 @@ mod tests { p.exp_regex(r#"Are you sure you want to override.*"#) .unwrap(); p.send_line("n").unwrap(); - std::thread::sleep(std::time::Duration::from_millis(100)); + p.flush().unwrap(); + std::thread::sleep(Duration::from_millis(100)); let mut expected = vec![]; File::open(&f).unwrap().read_to_end(&mut expected).unwrap(); - assert!( - !expected.len() > 100, - "Expected output to be printed to file" - ) + assert_eq!( + expected.as_slice(), + b"I'm a file!", + "Expected output file to remain unchanged" + ); } } diff --git a/tests/test_full_samples.rs b/tests/test_full_samples.rs index d2be7bc1..28b04a53 100644 --- a/tests/test_full_samples.rs +++ b/tests/test_full_samples.rs @@ -14,10 +14,10 @@ fn test_full_sample(path: impl AsRef, ok_count: usize, err_count: usize) { let mut actual_err_count = 0; for r in parser.records() { - if r.is_ok() { + if let Ok(r) = r { actual_ok_count += 1; if log::log_enabled!(Level::Debug) { - println!("{}", r.unwrap().data); + println!("{}", r.data); } } else { actual_err_count += 1; @@ -33,10 +33,10 @@ fn test_full_sample(path: impl AsRef, ok_count: usize, err_count: usize) { let mut actual_err_count = 0; for r in parser.records_json() { - if r.is_ok() { + if let Ok(r) = r { actual_ok_count += 1; if log::log_enabled!(Level::Debug) { - println!("{}", r.unwrap().data); + println!("{}", r.data); } } else { actual_err_count += 1; @@ -54,10 +54,10 @@ fn test_full_sample(path: impl AsRef, ok_count: usize, err_count: usize) { parser = parser.with_configuration(seperate_json_attributes); for r in parser.records_json() { - if r.is_ok() { + if let Ok(r) = r { actual_ok_count += 1; if log::log_enabled!(Level::Debug) { - println!("{}", r.unwrap().data); + println!("{}", r.data); } } else { actual_err_count += 1; diff --git a/tests/test_full_samples_streaming.rs b/tests/test_full_samples_streaming.rs index 52e54ea2..efa5daf6 100644 --- a/tests/test_full_samples_streaming.rs +++ b/tests/test_full_samples_streaming.rs @@ -15,10 +15,10 @@ fn test_full_sample_streaming(path: impl AsRef, ok_count: usize, err_count // Test streaming JSON parser for r in parser.records_json_stream() { - if r.is_ok() { + if let Ok(r) = r { actual_ok_count += 1; if log::log_enabled!(Level::Debug) { - println!("{}", r.unwrap().data); + println!("{}", r.data); } } else { actual_err_count += 1; @@ -40,10 +40,10 @@ fn test_full_sample_streaming(path: impl AsRef, ok_count: usize, err_count parser = parser.with_configuration(separate_json_attributes); for r in parser.records_json_stream() { - if r.is_ok() { + if let Ok(r) = r { actual_ok_count += 1; if log::log_enabled!(Level::Debug) { - println!("{}", r.unwrap().data); + println!("{}", r.data); } } else { actual_err_count += 1; diff --git a/tests/test_wevt_templates.rs b/tests/test_wevt_templates.rs new file mode 100644 index 00000000..73053520 --- /dev/null +++ b/tests/test_wevt_templates.rs @@ -0,0 +1,1482 @@ +mod fixtures; + +#[cfg(feature = "wevt_templates")] +mod wevt_templates { + use super::fixtures::CLI_TEST_LOCK; + use evtx::wevt_templates::manifest::{ + CrimManifest, EventKey, MapDefinition, WevtManifestError, + }; + use evtx::wevt_templates::{ResourceIdentifier, extract_wevt_template_resources}; + use evtx::wevt_templates::{ + render_template_definition_to_xml, + render_template_definition_to_xml_with_substitution_values, + }; + use std::fs; + use std::path::{Path, PathBuf}; + use std::process::Command; + use tempfile::tempdir; + + const MINIMAL_PE: &[u8] = include_bytes!("fixtures/wevt_template_minimal_pe.bin"); + const MINIMAL_RESOURCE_DATA: &[u8] = b"CRIM|K\0\0WEVTTEST"; + + fn sized_utf16_z_bytes(s: &str) -> Vec { + let u16_count = s.encode_utf16().count() as u32; + let size = 4 + u16_count * 2 + 2; // size prefix + utf16 + NUL + let mut out = Vec::with_capacity(size as usize); + out.extend_from_slice(&size.to_le_bytes()); + for cu in s.encode_utf16() { + out.extend_from_slice(&cu.to_le_bytes()); + } + out.extend_from_slice(&0u16.to_le_bytes()); + out + } + + fn wevt_layout_for_single_provider( + descriptor_count: usize, + unknown2_count: usize, + ) -> (u32, u32) { + let provider_data_off: u32 = 16 + 20; // CRIM header + 1 provider descriptor + let wevt_size: u32 = 20 + + 8u32.saturating_mul(descriptor_count as u32) + + 4u32.saturating_mul(unknown2_count as u32); + (provider_data_off, wevt_size) + } + + fn element_offsets_after_wevt( + provider_data_off: u32, + wevt_size: u32, + element_sizes: &[usize], + ) -> Vec { + let mut offs = Vec::with_capacity(element_sizes.len()); + let mut cur = provider_data_off + wevt_size; + for &sz in element_sizes { + offs.push(cur); + cur = cur.saturating_add(sz as u32); + } + offs + } + + fn build_crim_single_provider_blob( + provider_guid: [u8; 16], + wevt_message_id: u32, + unknown2: &[u32], + element_offsets: &[u32], + elements: &[Vec], + tail: &[u8], + ) -> Vec { + assert_eq!( + element_offsets.len(), + elements.len(), + "element_offsets/element vec length mismatch" + ); + + let (provider_data_off, wevt_size) = + wevt_layout_for_single_provider(elements.len(), unknown2.len()); + + if let Some(&first) = element_offsets.first() { + assert_eq!( + first, + provider_data_off + wevt_size, + "unexpected first element offset" + ); + } + + let mut blob = Vec::new(); + + // CRIM header (size patched after assembly). + blob.extend_from_slice(b"CRIM"); + blob.extend_from_slice(&0u32.to_le_bytes()); // size placeholder + blob.extend_from_slice(&3u16.to_le_bytes()); // major + blob.extend_from_slice(&1u16.to_le_bytes()); // minor + blob.extend_from_slice(&1u32.to_le_bytes()); // provider_count + + // provider descriptor + blob.extend_from_slice(&provider_guid); + blob.extend_from_slice(&provider_data_off.to_le_bytes()); + + // WEVT header + descriptors + unknown2 + blob.extend_from_slice(b"WEVT"); + blob.extend_from_slice(&wevt_size.to_le_bytes()); + blob.extend_from_slice(&wevt_message_id.to_le_bytes()); + blob.extend_from_slice(&(elements.len() as u32).to_le_bytes()); // descriptor count + blob.extend_from_slice(&(unknown2.len() as u32).to_le_bytes()); // unknown2 count + for &off in element_offsets { + blob.extend_from_slice(&off.to_le_bytes()); + blob.extend_from_slice(&0u32.to_le_bytes()); // unknown + } + for &v in unknown2 { + blob.extend_from_slice(&v.to_le_bytes()); + } + + // elements (must be appended in the same order as element_offsets were computed). + for el in elements { + blob.extend_from_slice(el); + } + + // trailing bytes (strings, etc.) + blob.extend_from_slice(tail); + + // Patch CRIM.size. + let total_size = u32::try_from(blob.len()).unwrap(); + blob[4..8].copy_from_slice(&total_size.to_le_bytes()); + + blob + } + + #[test] + fn it_extracts_wevt_template_from_minimal_synthetic_pe() { + let resources = + extract_wevt_template_resources(MINIMAL_PE).expect("extract should succeed"); + assert_eq!(resources.len(), 1); + + let r = &resources[0]; + assert_eq!(r.resource, ResourceIdentifier::Id(1)); + assert_eq!(r.lang_id, 1033); + assert_eq!(r.data.as_slice(), MINIMAL_RESOURCE_DATA); + } + + #[test] + fn cli_extracts_wevt_template_from_minimal_synthetic_pe() { + let _guard = CLI_TEST_LOCK.lock().unwrap(); + let d = tempdir().unwrap(); + let out_dir = d.path().join("out"); + std::fs::create_dir_all(&out_dir).unwrap(); + + let pe_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("wevt_template_minimal_pe.bin"); + + let mut cmd = Command::new(assert_cmd::cargo_bin!("evtx_dump")); + cmd.args([ + "extract-wevt-templates", + "--input", + pe_path.to_str().unwrap(), + "--output-dir", + out_dir.to_str().unwrap(), + "--overwrite", + ]); + + let out = cmd.output().unwrap(); + assert!( + out.status.success(), + "stdout: {}\nstderr: {}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + + // Expect a single JSONL line on stdout. + let stdout = String::from_utf8(out.stdout).unwrap(); + let line = stdout.lines().next().expect("expected one JSONL line"); + let v: serde_json::Value = serde_json::from_str(line).unwrap(); + + assert_eq!(v["resource"], 1); + assert_eq!(v["lang_id"], 1033); + + let out_path = PathBuf::from(v["output_path"].as_str().unwrap()); + assert!(out_path.starts_with(&out_dir)); + + let extracted = fs::read(&out_path).unwrap(); + assert_eq!(extracted.as_slice(), MINIMAL_RESOURCE_DATA); + } + + #[test] + fn it_parses_synthetic_crim_and_links_event_to_template_by_offset() { + // Minimal CRIM -> WEVT -> EVNT + TTBL with a single TEMP (no BinXML payload). + // + // This validates the core join key in libfwevt: `EVNT.template_offset` points to a `TEMP` + // definition offset (relative to the start of the CRIM blob). + let guid_bytes: [u8; 16] = [ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, + 0xff, 0x00, + ]; + + let provider_data_off: u32 = 16 + 20; + let wevt_size: u32 = 20 + 8 * 2; + let evnt_off: u32 = provider_data_off + wevt_size; + let evnt_size: u32 = 16 + 48; // header + 1 event + let ttbl_off: u32 = evnt_off + evnt_size; + + let temp_size: u32 = 40; + let ttbl_size: u32 = 12 + temp_size; + let temp_off: u32 = ttbl_off + 12; + + // TEMP has no items, so template_items_offset is either 0 or end-of-template. + let template_items_offset: u32 = temp_off + temp_size; + + // Build EVNT (1 event pointing at TEMP offset) + let mut evnt = Vec::with_capacity(evnt_size as usize); + evnt.extend_from_slice(b"EVNT"); + evnt.extend_from_slice(&evnt_size.to_le_bytes()); + evnt.extend_from_slice(&1u32.to_le_bytes()); // count + evnt.extend_from_slice(&0u32.to_le_bytes()); // unknown + // Event definition (48 bytes) + evnt.extend_from_slice(&7u16.to_le_bytes()); // event id + evnt.push(1u8); // version + evnt.push(0u8); // channel + evnt.push(0u8); // level + evnt.push(0u8); // opcode + evnt.extend_from_slice(&0u16.to_le_bytes()); // task + evnt.extend_from_slice(&0u64.to_le_bytes()); // keywords + evnt.extend_from_slice(&0xffffffffu32.to_le_bytes()); // message id + evnt.extend_from_slice(&temp_off.to_le_bytes()); // template_offset + evnt.extend_from_slice(&0u32.to_le_bytes()); // opcode_offset + evnt.extend_from_slice(&0u32.to_le_bytes()); // level_offset + evnt.extend_from_slice(&0u32.to_le_bytes()); // task_offset + evnt.extend_from_slice(&0u32.to_le_bytes()); // unknown_count + evnt.extend_from_slice(&0u32.to_le_bytes()); // unknown_offset + evnt.extend_from_slice(&0u32.to_le_bytes()); // flags + assert_eq!(evnt.len(), evnt_size as usize); + + // Build TTBL (1 TEMP) + let mut ttbl = Vec::with_capacity(ttbl_size as usize); + ttbl.extend_from_slice(b"TTBL"); + ttbl.extend_from_slice(&ttbl_size.to_le_bytes()); + ttbl.extend_from_slice(&1u32.to_le_bytes()); // template count + ttbl.extend_from_slice(b"TEMP"); + ttbl.extend_from_slice(&temp_size.to_le_bytes()); + ttbl.extend_from_slice(&0u32.to_le_bytes()); // item_descriptor_count + ttbl.extend_from_slice(&0u32.to_le_bytes()); // item_name_count + ttbl.extend_from_slice(&template_items_offset.to_le_bytes()); + ttbl.extend_from_slice(&1u32.to_le_bytes()); // event_type (observed as 1 for EventData) + ttbl.extend_from_slice(&guid_bytes); + assert_eq!(ttbl.len(), ttbl_size as usize); + + let total_size = (ttbl_off as usize) + ttbl.len(); + let mut blob = Vec::with_capacity(total_size); + + // CRIM + blob.extend_from_slice(b"CRIM"); + blob.extend_from_slice(&(total_size as u32).to_le_bytes()); // size + blob.extend_from_slice(&3u16.to_le_bytes()); // major + blob.extend_from_slice(&1u16.to_le_bytes()); // minor + blob.extend_from_slice(&1u32.to_le_bytes()); // provider_count + + // provider descriptor + blob.extend_from_slice(&[0u8; 16]); // provider GUID (unused here) + blob.extend_from_slice(&provider_data_off.to_le_bytes()); + + // WEVT header + 2 descriptors (EVNT + TTBL) + blob.extend_from_slice(b"WEVT"); + blob.extend_from_slice(&wevt_size.to_le_bytes()); + blob.extend_from_slice(&0xffffffffu32.to_le_bytes()); // message-table id + blob.extend_from_slice(&2u32.to_le_bytes()); // descriptor count + blob.extend_from_slice(&0u32.to_le_bytes()); // unknown2 count + // descriptor 0: EVNT + blob.extend_from_slice(&evnt_off.to_le_bytes()); + blob.extend_from_slice(&0u32.to_le_bytes()); + // descriptor 1: TTBL + blob.extend_from_slice(&ttbl_off.to_le_bytes()); + blob.extend_from_slice(&0u32.to_le_bytes()); + + // EVNT + TTBL + blob.extend_from_slice(&evnt); + blob.extend_from_slice(&ttbl); + + let manifest = CrimManifest::parse(&blob).expect("manifest parse should succeed"); + assert_eq!(manifest.providers.len(), 1); + let p = &manifest.providers[0]; + + let events = &p + .wevt + .elements + .events + .as_ref() + .expect("EVNT present") + .events; + assert_eq!(events.len(), 1); + assert_eq!(events[0].template_offset, Some(temp_off)); + + let tpl = p.template_by_offset(temp_off).expect("template resolved"); + assert_eq!(tpl.offset, temp_off); + assert_eq!(tpl.size, temp_size); + assert_eq!(tpl.item_descriptor_count, 0); + assert_eq!(tpl.template_items_offset, template_items_offset); + } + + #[test] + fn it_builds_manifest_index_and_dedupes_event_template_guids() { + // EVNT.size==0 + TTBL.size==0 branches, and build_index coverage (including the dedupe check). + let provider_guid = [0u8; 16]; + let wevt_message_id: u32 = 0x12345678; + + let descriptor_count = 2usize; // EVNT + TTBL + let unknown2: [u32; 0] = []; + let (provider_data_off, wevt_size) = + wevt_layout_for_single_provider(descriptor_count, unknown2.len()); + + let evnt_off = provider_data_off + wevt_size; + let evnt_count: u32 = 3; + let evnt_len = 16 + (48 * evnt_count as usize); + let ttbl_off = evnt_off + (evnt_len as u32); + + let temp_size: u32 = 40; + let temp_off = ttbl_off + 12; + + // EVNT (3 events: 1 with no template, 2 duplicates pointing at the same TEMP) + let mut evnt = Vec::with_capacity(evnt_len); + evnt.extend_from_slice(b"EVNT"); + evnt.extend_from_slice(&0u32.to_le_bytes()); // size==0 + evnt.extend_from_slice(&evnt_count.to_le_bytes()); + evnt.extend_from_slice(&0u32.to_le_bytes()); // unknown + + let mut push_event = |template_offset: u32| { + evnt.extend_from_slice(&7u16.to_le_bytes()); // event id + evnt.push(1u8); // version + evnt.push(0u8); // channel + evnt.push(0u8); // level + evnt.push(0u8); // opcode + evnt.extend_from_slice(&0u16.to_le_bytes()); // task + evnt.extend_from_slice(&0u64.to_le_bytes()); // keywords + evnt.extend_from_slice(&0u32.to_le_bytes()); // message id + evnt.extend_from_slice(&template_offset.to_le_bytes()); + evnt.extend_from_slice(&0u32.to_le_bytes()); // opcode_offset + evnt.extend_from_slice(&0u32.to_le_bytes()); // level_offset + evnt.extend_from_slice(&0u32.to_le_bytes()); // task_offset + evnt.extend_from_slice(&0u32.to_le_bytes()); // unknown_count + evnt.extend_from_slice(&0u32.to_le_bytes()); // unknown_offset + evnt.extend_from_slice(&0u32.to_le_bytes()); // flags + }; + + push_event(0); // None + push_event(temp_off); // Some + push_event(temp_off); // Some duplicate + assert_eq!(evnt.len(), evnt_len); + + // TTBL (size==0) with one TEMP. TEMP has no items and template_items_offset==0 (valid). + let ttbl_len: usize = 12 + (temp_size as usize); + let mut ttbl = Vec::with_capacity(ttbl_len); + ttbl.extend_from_slice(b"TTBL"); + ttbl.extend_from_slice(&0u32.to_le_bytes()); // size==0 + ttbl.extend_from_slice(&1u32.to_le_bytes()); // template count + + ttbl.extend_from_slice(b"TEMP"); + ttbl.extend_from_slice(&temp_size.to_le_bytes()); + ttbl.extend_from_slice(&0u32.to_le_bytes()); // item_descriptor_count + ttbl.extend_from_slice(&0u32.to_le_bytes()); // item_name_count + ttbl.extend_from_slice(&0u32.to_le_bytes()); // template_items_offset==0 + ttbl.extend_from_slice(&1u32.to_le_bytes()); // event_type + ttbl.extend_from_slice(&[0x11u8; 16]); // template guid + assert_eq!(ttbl.len(), ttbl_len); + + let total_size = (ttbl_off as usize) + ttbl.len(); + + let element_offsets = vec![evnt_off, ttbl_off]; + let elements = vec![evnt, ttbl]; + let blob = build_crim_single_provider_blob( + provider_guid, + wevt_message_id, + &unknown2, + &element_offsets, + &elements, + &[], + ); + assert_eq!(blob.len(), total_size); + + let manifest = CrimManifest::parse(&blob).expect("manifest parse should succeed"); + let idx = manifest.build_index(); + + let provider = &manifest.providers[0]; + let ttbl = provider + .wevt + .elements + .templates + .as_ref() + .expect("TTBL present"); + let tpl = &ttbl.templates[0]; + + let tpl_guid_str = tpl.guid.to_string(); + assert!( + idx.templates_by_guid.contains_key(&tpl_guid_str), + "expected templates_by_guid to contain template guid" + ); + + assert_eq!( + idx.event_to_template_guids.len(), + 1, + "expected only one unique EventKey entry" + ); + + let key = EventKey { + provider_guid: provider.guid.to_string(), + event_id: 7, + version: 1, + channel: 0, + level: 0, + opcode: 0, + task: 0, + keywords: 0, + }; + let mapped = idx + .event_to_template_guids + .get(&key) + .expect("expected EventKey mapping"); + assert_eq!(mapped.len(), 1, "expected deduped guid list"); + assert_eq!(mapped[0].to_string(), tpl.guid.to_string()); + } + + #[test] + fn it_labels_substitutions_using_template_item_names() { + fn name_hash_utf16(s: &str) -> u16 { + let mut hash: u32 = 0; + for cu in s.encode_utf16() { + hash = hash.wrapping_mul(65599).wrapping_add(u32::from(cu)); + } + (hash & 0xffff) as u16 + } + + fn push_inline_name(buf: &mut Vec, name: &str) { + let hash = name_hash_utf16(name); + buf.extend_from_slice(&hash.to_le_bytes()); + buf.extend_from_slice(&(name.encode_utf16().count() as u16).to_le_bytes()); + for cu in name.encode_utf16() { + buf.extend_from_slice(&cu.to_le_bytes()); + } + buf.extend_from_slice(&0u16.to_le_bytes()); + } + + // Build a minimal CRIM with one provider and one TTBL/TEMP that contains a BinXML fragment + // with a substitution token. The TEMP item descriptor provides a name for substitution 0. + let provider_data_off: u32 = 16 + 20; + let wevt_size: u32 = 28; // WEVT header (20) + 1 descriptor (8) + let ttbl_off: u32 = provider_data_off + wevt_size; + let temp_off: u32 = ttbl_off + 12; + + // BinXML fragment: {sub:0} + let mut binxml = Vec::new(); + binxml.extend_from_slice(&[0x0f, 0x01, 0x01, 0x00]); // StartOfStream + fragment header + + // + binxml.push(0x01); // OpenStartElement + binxml.extend_from_slice(&0xFFFFu16.to_le_bytes()); // dependency id + binxml.extend_from_slice(&0u32.to_le_bytes()); // data size (not enforced) + push_inline_name(&mut binxml, "EventData"); + binxml.push(0x02); // CloseStartElement + + // + binxml.push(0x01); // OpenStartElement + binxml.extend_from_slice(&0xFFFFu16.to_le_bytes()); // dependency id + binxml.extend_from_slice(&0u32.to_le_bytes()); // data size + push_inline_name(&mut binxml, "Data"); + binxml.push(0x02); // CloseStartElement + + // {sub:0} as a normal substitution, type=StringType (0x01) + binxml.push(0x0d); + binxml.extend_from_slice(&0u16.to_le_bytes()); + binxml.push(0x01); + + // + binxml.push(0x04); + binxml.push(0x04); + binxml.push(0x00); // EndOfStream + + let item_name = "Foo"; + let item_name_u16_count = item_name.encode_utf16().count() as u32; + let item_name_struct_size: u32 = 4 + item_name_u16_count * 2 + 2; // size + utf16 + NUL + + let descriptor_count: u32 = 1; + let name_count: u32 = 1; + let template_items_offset: u32 = temp_off + 40 + (binxml.len() as u32); + let name_offset: u32 = template_items_offset + 20; // right after 1 descriptor + + let temp_size: u32 = + 40 + (binxml.len() as u32) + 20 * descriptor_count + item_name_struct_size; + let ttbl_size: u32 = 12 + temp_size; + + let mut ttbl = Vec::with_capacity(ttbl_size as usize); + ttbl.extend_from_slice(b"TTBL"); + ttbl.extend_from_slice(&ttbl_size.to_le_bytes()); + ttbl.extend_from_slice(&1u32.to_le_bytes()); // template count + + // TEMP header + ttbl.extend_from_slice(b"TEMP"); + ttbl.extend_from_slice(&temp_size.to_le_bytes()); + ttbl.extend_from_slice(&descriptor_count.to_le_bytes()); + ttbl.extend_from_slice(&name_count.to_le_bytes()); + ttbl.extend_from_slice(&template_items_offset.to_le_bytes()); + ttbl.extend_from_slice(&1u32.to_le_bytes()); // event_type + ttbl.extend_from_slice(&[0x11u8; 16]); // template guid + + // BinXML fragment + ttbl.extend_from_slice(&binxml); + + // Template item descriptor (20 bytes) + ttbl.extend_from_slice(&0u32.to_le_bytes()); // unknown1 + ttbl.push(0x01); // inType (UnicodeString) + ttbl.push(0x01); // outType (xs:string) + ttbl.extend_from_slice(&0u16.to_le_bytes()); // unknown3 + ttbl.extend_from_slice(&0u32.to_le_bytes()); // unknown4 + ttbl.extend_from_slice(&1u16.to_le_bytes()); // count + ttbl.extend_from_slice(&0u16.to_le_bytes()); // length + ttbl.extend_from_slice(&name_offset.to_le_bytes()); + + // Template item name (size-prefixed utf16 + NUL) + ttbl.extend_from_slice(&item_name_struct_size.to_le_bytes()); + for cu in item_name.encode_utf16() { + ttbl.extend_from_slice(&cu.to_le_bytes()); + } + ttbl.extend_from_slice(&0u16.to_le_bytes()); + + assert_eq!(ttbl.len(), ttbl_size as usize); + + let total_size = (ttbl_off as usize) + ttbl.len(); + let mut blob = Vec::with_capacity(total_size); + + // CRIM + blob.extend_from_slice(b"CRIM"); + blob.extend_from_slice(&(total_size as u32).to_le_bytes()); // size + blob.extend_from_slice(&3u16.to_le_bytes()); // major + blob.extend_from_slice(&1u16.to_le_bytes()); // minor + blob.extend_from_slice(&1u32.to_le_bytes()); // provider_count + + // provider descriptor + blob.extend_from_slice(&[0x22u8; 16]); // provider GUID (unused here) + blob.extend_from_slice(&provider_data_off.to_le_bytes()); + + // WEVT header + 1 descriptor (TTBL) + blob.extend_from_slice(b"WEVT"); + blob.extend_from_slice(&wevt_size.to_le_bytes()); + blob.extend_from_slice(&0xffffffffu32.to_le_bytes()); + blob.extend_from_slice(&1u32.to_le_bytes()); // descriptor count + blob.extend_from_slice(&0u32.to_le_bytes()); // unknown2 count + blob.extend_from_slice(&ttbl_off.to_le_bytes()); // TTBL offset + blob.extend_from_slice(&0u32.to_le_bytes()); // unknown + + // TTBL + blob.extend_from_slice(&ttbl); + + let manifest = CrimManifest::parse(&blob).expect("manifest parse should succeed"); + let provider = &manifest.providers[0]; + let ttbl = provider + .wevt + .elements + .templates + .as_ref() + .expect("TTBL present"); + let tpl = &ttbl.templates[0]; + + assert_eq!(tpl.items.len(), 1); + assert_eq!(tpl.items[0].name.as_deref(), Some(item_name)); + + let xml = render_template_definition_to_xml(tpl, encoding::all::WINDOWS_1252) + .expect("render should succeed"); + assert!( + xml.contains("{sub:0:Foo}"), + "expected substitution placeholder to include item name, got: {xml}" + ); + + let subs = vec!["BAR".to_string()]; + let applied = render_template_definition_to_xml_with_substitution_values( + tpl, + &subs, + encoding::all::WINDOWS_1252, + ) + .expect("render with substitutions should succeed"); + assert!( + applied.contains("BAR") && !applied.contains("{sub:"), + "expected placeholders to be replaced, got: {applied}" + ); + } + + fn build_defs_manifest_with_sizes(size_zero: bool) -> CrimManifest<'static> { + // Build a single-provider CRIM with CHAN/KEYW/LEVL/OPCO/TASK elements and out-of-band names. + let provider_guid = [0x44u8; 16]; + let wevt_message_id: u32 = 0x0bad_f00d; + let unknown2 = [0xdead_beefu32, 0xcafe_babeu32]; + + let chan_len: usize = 12 + 16; // header + 1 channel + let keyw_len: usize = 12 + 16; // header + 1 keyword + let levl_len: usize = 12 + 12; // header + 1 level + let opco_len: usize = 12 + 12; // header + 1 opcode + let task_len: usize = 12 + 28; // header + 1 task + + let element_sizes = [chan_len, keyw_len, levl_len, opco_len, task_len]; + let descriptor_count = element_sizes.len(); + let (provider_data_off, wevt_size) = + wevt_layout_for_single_provider(descriptor_count, unknown2.len()); + let element_offsets = + element_offsets_after_wevt(provider_data_off, wevt_size, &element_sizes); + + let tail_off = provider_data_off + wevt_size + (element_sizes.iter().sum::() as u32); + let chan_name = sized_utf16_z_bytes("ChanA"); + let keyw_name = sized_utf16_z_bytes("KeywA"); + let levl_name = sized_utf16_z_bytes("LevlA"); + let opco_name = sized_utf16_z_bytes("OpcoA"); + let task_name = sized_utf16_z_bytes("TaskA"); + + let chan_name_off = tail_off; + let keyw_name_off = chan_name_off + (chan_name.len() as u32); + let levl_name_off = keyw_name_off + (keyw_name.len() as u32); + let opco_name_off = levl_name_off + (levl_name.len() as u32); + let task_name_off = opco_name_off + (opco_name.len() as u32); + + let mut tail = Vec::new(); + tail.extend_from_slice(&chan_name); + tail.extend_from_slice(&keyw_name); + tail.extend_from_slice(&levl_name); + tail.extend_from_slice(&opco_name); + tail.extend_from_slice(&task_name); + + let size_or = |v: usize| if size_zero { 0u32 } else { v as u32 }; + + // CHAN + let mut chan = Vec::with_capacity(chan_len); + chan.extend_from_slice(b"CHAN"); + chan.extend_from_slice(&size_or(chan_len).to_le_bytes()); + chan.extend_from_slice(&1u32.to_le_bytes()); // count + chan.extend_from_slice(&42u32.to_le_bytes()); // identifier + chan.extend_from_slice(&chan_name_off.to_le_bytes()); + chan.extend_from_slice(&0x1111u32.to_le_bytes()); // unknown + chan.extend_from_slice(&0x2222u32.to_le_bytes()); // message_identifier (Some) + assert_eq!(chan.len(), chan_len); + + // KEYW + let mut keyw = Vec::with_capacity(keyw_len); + keyw.extend_from_slice(b"KEYW"); + keyw.extend_from_slice(&size_or(keyw_len).to_le_bytes()); + keyw.extend_from_slice(&1u32.to_le_bytes()); // count + keyw.extend_from_slice(&0x1122334455667788u64.to_le_bytes()); // identifier + keyw.extend_from_slice(&0xffffffffu32.to_le_bytes()); // message_identifier (None) + keyw.extend_from_slice(&keyw_name_off.to_le_bytes()); // data_offset + assert_eq!(keyw.len(), keyw_len); + + // LEVL + let mut levl = Vec::with_capacity(levl_len); + levl.extend_from_slice(b"LEVL"); + levl.extend_from_slice(&size_or(levl_len).to_le_bytes()); + levl.extend_from_slice(&1u32.to_le_bytes()); // count + levl.extend_from_slice(&5u32.to_le_bytes()); // identifier + levl.extend_from_slice(&0x3333u32.to_le_bytes()); // message_identifier (Some) + levl.extend_from_slice(&levl_name_off.to_le_bytes()); // data_offset + assert_eq!(levl.len(), levl_len); + + // OPCO + let mut opco = Vec::with_capacity(opco_len); + opco.extend_from_slice(b"OPCO"); + opco.extend_from_slice(&size_or(opco_len).to_le_bytes()); + opco.extend_from_slice(&1u32.to_le_bytes()); // count + opco.extend_from_slice(&9u32.to_le_bytes()); // identifier + opco.extend_from_slice(&0xffffffffu32.to_le_bytes()); // message_identifier (None) + opco.extend_from_slice(&opco_name_off.to_le_bytes()); // data_offset + assert_eq!(opco.len(), opco_len); + + // TASK + let mut task = Vec::with_capacity(task_len); + task.extend_from_slice(b"TASK"); + task.extend_from_slice(&size_or(task_len).to_le_bytes()); + task.extend_from_slice(&1u32.to_le_bytes()); // count + task.extend_from_slice(&7u32.to_le_bytes()); // identifier + task.extend_from_slice(&0x4444u32.to_le_bytes()); // message_identifier (Some) + task.extend_from_slice(&[0x33u8; 16]); // mui_identifier + task.extend_from_slice(&task_name_off.to_le_bytes()); // data_offset + assert_eq!(task.len(), task_len); + + let elements = vec![chan, keyw, levl, opco, task]; + let blob = build_crim_single_provider_blob( + provider_guid, + wevt_message_id, + &unknown2, + &element_offsets, + &elements, + &tail, + ); + + // Leak to extend lifetime for test convenience. + let blob: &'static [u8] = Box::leak(blob.into_boxed_slice()); + CrimManifest::parse(blob).expect("manifest parse should succeed") + } + + #[test] + fn it_parses_common_definition_elements_with_explicit_sizes() { + let manifest = build_defs_manifest_with_sizes(false); + let provider = &manifest.providers[0]; + + assert_eq!(provider.wevt.message_identifier, Some(0x0bad_f00d)); + assert_eq!(provider.wevt.unknown2, vec![0xdead_beef, 0xcafe_babe]); + + let chan = provider + .wevt + .elements + .channels + .as_ref() + .expect("CHAN present"); + assert_eq!(chan.channels.len(), 1); + assert_eq!(chan.channels[0].name.as_deref(), Some("ChanA")); + assert_eq!(chan.channels[0].message_identifier, Some(0x2222)); + + let keyw = provider + .wevt + .elements + .keywords + .as_ref() + .expect("KEYW present"); + assert_eq!(keyw.keywords.len(), 1); + assert_eq!(keyw.keywords[0].name.as_deref(), Some("KeywA")); + assert_eq!(keyw.keywords[0].message_identifier, None); + + let levl = provider + .wevt + .elements + .levels + .as_ref() + .expect("LEVL present"); + assert_eq!(levl.levels.len(), 1); + assert_eq!(levl.levels[0].name.as_deref(), Some("LevlA")); + assert_eq!(levl.levels[0].message_identifier, Some(0x3333)); + + let opco = provider + .wevt + .elements + .opcodes + .as_ref() + .expect("OPCO present"); + assert_eq!(opco.opcodes.len(), 1); + assert_eq!(opco.opcodes[0].name.as_deref(), Some("OpcoA")); + assert_eq!(opco.opcodes[0].message_identifier, None); + + let task = provider.wevt.elements.tasks.as_ref().expect("TASK present"); + assert_eq!(task.tasks.len(), 1); + assert_eq!(task.tasks[0].name.as_deref(), Some("TaskA")); + assert_eq!(task.tasks[0].message_identifier, Some(0x4444)); + } + + #[test] + fn it_parses_common_definition_elements_with_size_zero_compat() { + let manifest = build_defs_manifest_with_sizes(true); + let provider = &manifest.providers[0]; + + assert!(provider.wevt.elements.channels.is_some()); + assert!(provider.wevt.elements.keywords.is_some()); + assert!(provider.wevt.elements.levels.is_some()); + assert!(provider.wevt.elements.opcodes.is_some()); + assert!(provider.wevt.elements.tasks.is_some()); + } + + #[test] + fn it_parses_maps_with_implied_first_offset_and_size_zero() { + let provider_guid = [0x55u8; 16]; + let wevt_message_id: u32 = 0xffffffff; // None + let unknown2: [u32; 0] = []; + + let descriptor_count = 1usize; // MAPS only + let (provider_data_off, wevt_size) = + wevt_layout_for_single_provider(descriptor_count, unknown2.len()); + let maps_off = provider_data_off + wevt_size; + + let map_count: u32 = 3; + let vmap_entry_count: u32 = 2; + let vmap_size: u32 = 16 + 8 * vmap_entry_count + 2; // header + entries + trailing + + let map1_off = (maps_off + 16 + 8) + vmap_size; // implied_first + vmap_size + let map2_off = map1_off + 4; + + let maps_len: usize = 16 + 8 + (vmap_size as usize) + 4 + 4; + let map_string_off = maps_off + (maps_len as u32); + + let mut maps = Vec::with_capacity(maps_len); + maps.extend_from_slice(b"MAPS"); + maps.extend_from_slice(&0u32.to_le_bytes()); // size==0 + maps.extend_from_slice(&map_count.to_le_bytes()); + maps.extend_from_slice(&0u32.to_le_bytes()); // first_map_offset==0 => implied + + // Remaining offsets array (count-1). + maps.extend_from_slice(&map1_off.to_le_bytes()); + maps.extend_from_slice(&map2_off.to_le_bytes()); + + // VMAP at implied_first = maps_off + 16 + (count-1)*4 = maps_off + 24 + maps.extend_from_slice(b"VMAP"); + maps.extend_from_slice(&vmap_size.to_le_bytes()); + maps.extend_from_slice(&map_string_off.to_le_bytes()); + maps.extend_from_slice(&vmap_entry_count.to_le_bytes()); + // entries + maps.extend_from_slice(&1u32.to_le_bytes()); + maps.extend_from_slice(&0xffffffffu32.to_le_bytes()); // None + maps.extend_from_slice(&2u32.to_le_bytes()); + maps.extend_from_slice(&1234u32.to_le_bytes()); // Some + // trailing + maps.extend_from_slice(&[0xaa, 0xbb]); + + // BMAP + maps.extend_from_slice(b"BMAP"); + // unknown map type + maps.extend_from_slice(b"ZZZZ"); + + assert_eq!(maps.len(), maps_len); + + let tail = sized_utf16_z_bytes("MapStr"); + let element_offsets = vec![maps_off]; + let elements = vec![maps]; + let blob = build_crim_single_provider_blob( + provider_guid, + wevt_message_id, + &unknown2, + &element_offsets, + &elements, + &tail, + ); + + let manifest = CrimManifest::parse(&blob).expect("manifest parse should succeed"); + let provider = &manifest.providers[0]; + assert_eq!(provider.wevt.message_identifier, None); + + let maps = provider.wevt.elements.maps.as_ref().expect("MAPS present"); + assert_eq!(maps.maps.len(), 3); + + match &maps.maps[0] { + MapDefinition::ValueMap(v) => { + assert_eq!(v.size, vmap_size); + assert_eq!(v.map_string.as_deref(), Some("MapStr")); + assert_eq!(v.entries.len(), 2); + assert_eq!(v.entries[0].message_identifier, None); + assert_eq!(v.entries[1].message_identifier, Some(1234)); + assert_eq!(v.trailing, &[0xaa, 0xbb]); + } + _ => panic!("expected VMAP first"), + } + + match &maps.maps[1] { + MapDefinition::Bitmap(b) => { + assert_eq!(b.data, b"BMAP"); + } + _ => panic!("expected BMAP second"), + } + + match &maps.maps[2] { + MapDefinition::Unknown { + signature, data, .. + } => { + assert_eq!(signature, b"ZZZZ"); + assert_eq!(data.len(), 4, "unknown map types are capped"); + } + _ => panic!("expected unknown map third"), + } + } + + #[test] + fn it_parses_maps_with_explicit_size_and_first_offset() { + let provider_guid = [0x55u8; 16]; + let wevt_message_id: u32 = 0xffffffff; + let unknown2: [u32; 0] = []; + + let descriptor_count = 1usize; // MAPS only + let (provider_data_off, wevt_size) = + wevt_layout_for_single_provider(descriptor_count, unknown2.len()); + let maps_off = provider_data_off + wevt_size; + + let map_count: u32 = 3; + let vmap_entry_count: u32 = 1; + let vmap_size: u32 = 16 + 8 * vmap_entry_count; // no trailing + + let implied_first = maps_off + 16 + 8; + let map0_off = implied_first; + let map1_off = map0_off + vmap_size; + let map2_off = map1_off + 4; + + let maps_len: usize = 16 + 8 + (vmap_size as usize) + 4 + 4; + let map_string_off = maps_off + (maps_len as u32); + + let mut maps = Vec::with_capacity(maps_len); + maps.extend_from_slice(b"MAPS"); + maps.extend_from_slice(&(maps_len as u32).to_le_bytes()); // explicit size + maps.extend_from_slice(&map_count.to_le_bytes()); + maps.extend_from_slice(&map0_off.to_le_bytes()); // explicit first map offset + + maps.extend_from_slice(&map1_off.to_le_bytes()); + maps.extend_from_slice(&map2_off.to_le_bytes()); + + maps.extend_from_slice(b"VMAP"); + maps.extend_from_slice(&vmap_size.to_le_bytes()); + maps.extend_from_slice(&map_string_off.to_le_bytes()); + maps.extend_from_slice(&vmap_entry_count.to_le_bytes()); + maps.extend_from_slice(&0u32.to_le_bytes()); + maps.extend_from_slice(&0xffffffffu32.to_le_bytes()); + + maps.extend_from_slice(b"BMAP"); + maps.extend_from_slice(b"ZZZZ"); + assert_eq!(maps.len(), maps_len); + + let tail = sized_utf16_z_bytes("X"); + let element_offsets = vec![maps_off]; + let elements = vec![maps]; + let blob = build_crim_single_provider_blob( + provider_guid, + wevt_message_id, + &unknown2, + &element_offsets, + &elements, + &tail, + ); + + let manifest = CrimManifest::parse(&blob).expect("manifest parse should succeed"); + let provider = &manifest.providers[0]; + let maps = provider.wevt.elements.maps.as_ref().expect("MAPS present"); + assert_eq!(maps.maps.len(), 3); + } + + #[test] + fn it_captures_unknown_provider_elements() { + let provider_guid = [0x66u8; 16]; + let wevt_message_id: u32 = 0xffffffff; + let unknown2: [u32; 0] = []; + + let descriptor_count = 1usize; + let (provider_data_off, wevt_size) = + wevt_layout_for_single_provider(descriptor_count, unknown2.len()); + let unk_off = provider_data_off + wevt_size; + + let mut unk = Vec::new(); + unk.extend_from_slice(b"ZZZZ"); + unk.extend_from_slice(&12u32.to_le_bytes()); + unk.extend_from_slice(&0x01020304u32.to_le_bytes()); + assert_eq!(unk.len(), 12); + + let element_offsets = vec![unk_off]; + let elements = vec![unk]; + let blob = build_crim_single_provider_blob( + provider_guid, + wevt_message_id, + &unknown2, + &element_offsets, + &elements, + &[], + ); + + let manifest = CrimManifest::parse(&blob).expect("manifest parse should succeed"); + let provider = &manifest.providers[0]; + assert_eq!(provider.wevt.elements.unknown.len(), 1); + let u = &provider.wevt.elements.unknown[0]; + assert_eq!(u.signature, *b"ZZZZ"); + assert_eq!(u.offset, unk_off); + assert_eq!(u.size, 12); + assert_eq!(u.data.len(), 12); + } + + #[test] + fn it_reports_wevt_manifest_error_variants() { + // InvalidSignature + let err = CrimManifest::parse(b"NOPE1234").unwrap_err(); + match err { + WevtManifestError::InvalidSignature { + offset, + expected, + found, + } => { + assert_eq!(offset, 0); + assert_eq!(expected, *b"CRIM"); + assert_eq!(found, *b"NOPE"); + } + other => panic!("unexpected error: {other:?}"), + } + + // Truncated + let err = CrimManifest::parse(b"CRI").unwrap_err(); + assert!(matches!(err, WevtManifestError::Truncated { .. })); + + // SizeOutOfBounds: CRIM.size bigger than buffer. + let mut blob = Vec::new(); + blob.extend_from_slice(b"CRIM"); + blob.extend_from_slice(&100u32.to_le_bytes()); // size + blob.extend_from_slice(&3u16.to_le_bytes()); + blob.extend_from_slice(&1u16.to_le_bytes()); + blob.extend_from_slice(&0u32.to_le_bytes()); // provider_count + let err = CrimManifest::parse(&blob).unwrap_err(); + assert!(matches!( + err, + WevtManifestError::SizeOutOfBounds { + what: "CRIM.size", + .. + } + )); + + // CountOutOfBounds: TEMP has item_descriptor_count==0 but item_name_count!=0. + let provider_guid = [0x77u8; 16]; + let wevt_message_id: u32 = 0xffffffff; + let unknown2: [u32; 0] = []; + let descriptor_count = 1usize; + let (provider_data_off, wevt_size) = + wevt_layout_for_single_provider(descriptor_count, unknown2.len()); + let ttbl_off = provider_data_off + wevt_size; + + let temp_size: u32 = 40; + let ttbl_size: u32 = 12 + temp_size; + let mut ttbl = Vec::with_capacity(ttbl_size as usize); + ttbl.extend_from_slice(b"TTBL"); + ttbl.extend_from_slice(&ttbl_size.to_le_bytes()); + ttbl.extend_from_slice(&1u32.to_le_bytes()); + ttbl.extend_from_slice(b"TEMP"); + ttbl.extend_from_slice(&temp_size.to_le_bytes()); + ttbl.extend_from_slice(&0u32.to_le_bytes()); // item_descriptor_count + ttbl.extend_from_slice(&1u32.to_le_bytes()); // item_name_count (invalid) + ttbl.extend_from_slice(&0u32.to_le_bytes()); // template_items_offset + ttbl.extend_from_slice(&1u32.to_le_bytes()); // event_type + ttbl.extend_from_slice(&[0u8; 16]); + assert_eq!(ttbl.len(), ttbl_size as usize); + + let element_offsets = vec![ttbl_off]; + let elements = vec![ttbl]; + let blob = build_crim_single_provider_blob( + provider_guid, + wevt_message_id, + &unknown2, + &element_offsets, + &elements, + &[], + ); + let err = CrimManifest::parse(&blob).unwrap_err(); + assert!(matches!( + err, + WevtManifestError::CountOutOfBounds { what, .. } + if what.starts_with("TEMP.item_name_count") + )); + + // OffsetOutOfBounds: TEMP.template_items_offset < template offset when item_descriptor_count>0. + let temp_size: u32 = 40; + let ttbl_size: u32 = 12 + temp_size; + let mut ttbl = Vec::with_capacity(ttbl_size as usize); + ttbl.extend_from_slice(b"TTBL"); + ttbl.extend_from_slice(&ttbl_size.to_le_bytes()); + ttbl.extend_from_slice(&1u32.to_le_bytes()); + ttbl.extend_from_slice(b"TEMP"); + ttbl.extend_from_slice(&temp_size.to_le_bytes()); + ttbl.extend_from_slice(&1u32.to_le_bytes()); // item_descriptor_count + ttbl.extend_from_slice(&0u32.to_le_bytes()); // item_name_count + ttbl.extend_from_slice(&0u32.to_le_bytes()); // template_items_offset (invalid here) + ttbl.extend_from_slice(&1u32.to_le_bytes()); // event_type + ttbl.extend_from_slice(&[0u8; 16]); + assert_eq!(ttbl.len(), ttbl_size as usize); + + let element_offsets = vec![ttbl_off]; + let elements = vec![ttbl]; + let blob = build_crim_single_provider_blob( + provider_guid, + wevt_message_id, + &unknown2, + &element_offsets, + &elements, + &[], + ); + let err = CrimManifest::parse(&blob).unwrap_err(); + assert!(matches!( + err, + WevtManifestError::OffsetOutOfBounds { + what: "TEMP.template_items_offset", + .. + } + )); + + // InvalidUtf16String: CHAN name string has odd byte count. + let descriptor_count = 1usize; // CHAN only + let (provider_data_off, wevt_size) = + wevt_layout_for_single_provider(descriptor_count, unknown2.len()); + let chan_off = provider_data_off + wevt_size; + let chan_len: usize = 12 + 16; + let name_off = chan_off + (chan_len as u32); + + let mut chan = Vec::with_capacity(chan_len); + chan.extend_from_slice(b"CHAN"); + chan.extend_from_slice(&(chan_len as u32).to_le_bytes()); + chan.extend_from_slice(&1u32.to_le_bytes()); + chan.extend_from_slice(&1u32.to_le_bytes()); + chan.extend_from_slice(&name_off.to_le_bytes()); + chan.extend_from_slice(&0u32.to_le_bytes()); + chan.extend_from_slice(&0xffffffffu32.to_le_bytes()); + assert_eq!(chan.len(), chan_len); + + let mut bad = Vec::new(); + bad.extend_from_slice(&5u32.to_le_bytes()); // size (4 + 1 byte) + bad.push(0u8); + + let element_offsets = vec![chan_off]; + let elements = vec![chan]; + let blob = build_crim_single_provider_blob( + provider_guid, + wevt_message_id, + &unknown2, + &element_offsets, + &elements, + &bad, + ); + let err = CrimManifest::parse(&blob).unwrap_err(); + assert!(matches!( + err, + WevtManifestError::InvalidUtf16String { + what: "CHAN name", + .. + } + )); + } + + #[test] + fn it_errors_when_template_item_name_offset_overlaps_descriptor_table() { + // Cover parse_template_items' boundary enforcement between descriptor table and name table. + let provider_guid = [0x88u8; 16]; + let wevt_message_id: u32 = 0xffffffff; + let unknown2: [u32; 0] = []; + + let descriptor_count = 1usize; // TTBL only + let (provider_data_off, wevt_size) = + wevt_layout_for_single_provider(descriptor_count, unknown2.len()); + let ttbl_off = provider_data_off + wevt_size; + let temp_off = ttbl_off + 12; + + let temp_size: u32 = 60; // 40 header + 20 descriptor + let ttbl_size: u32 = 12 + temp_size; + let template_items_offset: u32 = temp_off + 40; // descriptor table begins immediately after TEMP header + let name_offset: u32 = template_items_offset; // overlaps descriptor table => should error + + let mut ttbl = Vec::with_capacity(ttbl_size as usize); + ttbl.extend_from_slice(b"TTBL"); + ttbl.extend_from_slice(&ttbl_size.to_le_bytes()); + ttbl.extend_from_slice(&1u32.to_le_bytes()); // template count + + ttbl.extend_from_slice(b"TEMP"); + ttbl.extend_from_slice(&temp_size.to_le_bytes()); + ttbl.extend_from_slice(&1u32.to_le_bytes()); // item_descriptor_count + ttbl.extend_from_slice(&1u32.to_le_bytes()); // item_name_count + ttbl.extend_from_slice(&template_items_offset.to_le_bytes()); + ttbl.extend_from_slice(&1u32.to_le_bytes()); // event_type + ttbl.extend_from_slice(&[0u8; 16]); + + // One template item descriptor (20 bytes). Name offset points inside this table. + ttbl.extend_from_slice(&0u32.to_le_bytes()); // unknown1 + ttbl.push(0x01); // inType + ttbl.push(0x01); // outType + ttbl.extend_from_slice(&0u16.to_le_bytes()); // unknown3 + ttbl.extend_from_slice(&0u32.to_le_bytes()); // unknown4 + ttbl.extend_from_slice(&1u16.to_le_bytes()); // count + ttbl.extend_from_slice(&0u16.to_le_bytes()); // length + ttbl.extend_from_slice(&name_offset.to_le_bytes()); + + assert_eq!(ttbl.len(), ttbl_size as usize); + + let element_offsets = vec![ttbl_off]; + let elements = vec![ttbl]; + let blob = build_crim_single_provider_blob( + provider_guid, + wevt_message_id, + &unknown2, + &element_offsets, + &elements, + &[], + ); + + let err = CrimManifest::parse(&blob).unwrap_err(); + assert!(matches!( + err, + WevtManifestError::OffsetOutOfBounds { + what: "template item name_offset overlaps descriptor table", + .. + } + )); + } + + #[test] + fn it_errors_on_invalid_utf16_surrogate_in_names() { + // Drive decode_utf16_z() through the String::from_utf16 error path (invalid surrogate). + let provider_guid = [0x99u8; 16]; + let wevt_message_id: u32 = 0xffffffff; + let unknown2: [u32; 0] = []; + + let descriptor_count = 1usize; // CHAN only + let (provider_data_off, wevt_size) = + wevt_layout_for_single_provider(descriptor_count, unknown2.len()); + let chan_off = provider_data_off + wevt_size; + let chan_len: usize = 12 + 16; + let name_off = chan_off + (chan_len as u32); + + let mut chan = Vec::with_capacity(chan_len); + chan.extend_from_slice(b"CHAN"); + chan.extend_from_slice(&(chan_len as u32).to_le_bytes()); + chan.extend_from_slice(&1u32.to_le_bytes()); + chan.extend_from_slice(&1u32.to_le_bytes()); + chan.extend_from_slice(&name_off.to_le_bytes()); + chan.extend_from_slice(&0u32.to_le_bytes()); + chan.extend_from_slice(&0xffffffffu32.to_le_bytes()); + assert_eq!(chan.len(), chan_len); + + // size=8, payload = [0xD800, 0x0000] => invalid (unpaired surrogate). + let mut bad = Vec::new(); + bad.extend_from_slice(&8u32.to_le_bytes()); + bad.extend_from_slice(&0xD800u16.to_le_bytes()); + bad.extend_from_slice(&0u16.to_le_bytes()); + + let element_offsets = vec![chan_off]; + let elements = vec![chan]; + let blob = build_crim_single_provider_blob( + provider_guid, + wevt_message_id, + &unknown2, + &element_offsets, + &elements, + &bad, + ); + let err = CrimManifest::parse(&blob).unwrap_err(); + assert!(matches!( + err, + WevtManifestError::InvalidUtf16String { + what: "CHAN name", + .. + } + )); + } + + #[test] + fn it_reports_vmap_error_paths() { + // Cover parse_vmap() Truncated and SizeOutOfBounds branches via MAPS. + let provider_guid = [0xaau8; 16]; + let wevt_message_id: u32 = 0xffffffff; + let unknown2: [u32; 0] = []; + + let descriptor_count = 1usize; // MAPS only + let (provider_data_off, wevt_size) = + wevt_layout_for_single_provider(descriptor_count, unknown2.len()); + let maps_off = provider_data_off + wevt_size; + + // Case 1: VMAP slice < 16 => Truncated { what: "VMAP header" }. + { + let map_count: u32 = 2; + let implied_first = maps_off + 16 + 4; + let map0_off = implied_first; + let map1_off = map0_off + 8; // only 8 bytes available for VMAP + let maps_len: usize = 16 + 4 + 8 + 4; + + let mut maps = Vec::with_capacity(maps_len); + maps.extend_from_slice(b"MAPS"); + maps.extend_from_slice(&(maps_len as u32).to_le_bytes()); + maps.extend_from_slice(&map_count.to_le_bytes()); + maps.extend_from_slice(&0u32.to_le_bytes()); // implied first + maps.extend_from_slice(&map1_off.to_le_bytes()); + maps.extend_from_slice(b"VMAP"); + maps.extend_from_slice(&0u32.to_le_bytes()); // dummy + maps.extend_from_slice(b"BMAP"); + assert_eq!(maps.len(), maps_len); + + let element_offsets = vec![maps_off]; + let elements = vec![maps]; + let blob = build_crim_single_provider_blob( + provider_guid, + wevt_message_id, + &unknown2, + &element_offsets, + &elements, + &[], + ); + let err = CrimManifest::parse(&blob).unwrap_err(); + assert!(matches!( + err, + WevtManifestError::Truncated { what: "VMAP header", offset, .. } + if offset == map0_off + )); + } + + // Case 2: VMAP.size larger than available slice => SizeOutOfBounds { what: "VMAP.size" }. + { + let map_count: u32 = 2; + let implied_first = maps_off + 16 + 4; + let map0_off = implied_first; + let map1_off = map0_off + 16; // exactly 16 bytes available + let maps_len: usize = 16 + 4 + 16 + 4; + + let mut maps = Vec::with_capacity(maps_len); + maps.extend_from_slice(b"MAPS"); + maps.extend_from_slice(&(maps_len as u32).to_le_bytes()); + maps.extend_from_slice(&map_count.to_le_bytes()); + maps.extend_from_slice(&0u32.to_le_bytes()); + maps.extend_from_slice(&map1_off.to_le_bytes()); + + maps.extend_from_slice(b"VMAP"); + maps.extend_from_slice(&32u32.to_le_bytes()); // size > slice len + maps.extend_from_slice(&0u32.to_le_bytes()); // map_string_offset + maps.extend_from_slice(&0u32.to_le_bytes()); // entry_count + maps.extend_from_slice(b"BMAP"); + assert_eq!(maps.len(), maps_len); + + let element_offsets = vec![maps_off]; + let elements = vec![maps]; + let blob = build_crim_single_provider_blob( + provider_guid, + wevt_message_id, + &unknown2, + &element_offsets, + &elements, + &[], + ); + let err = CrimManifest::parse(&blob).unwrap_err(); + assert!(matches!( + err, + WevtManifestError::SizeOutOfBounds { what: "VMAP.size", offset, .. } + if offset == map0_off + )); + } + } +} + +#[cfg(feature = "wevt_templates")] +mod wevt_templates_research { + use evtx::wevt_templates::manifest::CrimManifest; + use evtx::wevt_templates::parse_wevt_binxml_fragment; + use evtx::wevt_templates::{ + ResourceIdentifier, extract_temp_templates_from_wevt_blob, extract_wevt_template_resources, + }; + use std::fs; + use std::path::Path; + use std::process::Command; + use tempfile::tempdir; + + #[test] + fn it_finds_temp_entries_in_a_synthetic_ttbl_blob() { + // Minimal CRIM + WEVT + TTBL with a single TEMP entry (no BinXML payload). + // + // This is structured according to libfwevt's "Windows Event manifest binary format": + // CRIM header -> provider descriptor -> WEVT header -> provider element descriptor -> TTBL -> TEMP. + let guid_bytes: [u8; 16] = [ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, + 0xff, 0x00, + ]; + let temp_size: u32 = 40; + let ttbl_size: u32 = 12 + temp_size; + + // Layout sizes: + // CRIM header: 16 + // provider descriptor: 20 + // WEVT header: 20 + 8 * 1 descriptor = 28 + let provider_data_off: u32 = 16 + 20; + let ttbl_off: u32 = provider_data_off + 28; + + let mut ttbl = Vec::with_capacity(ttbl_size as usize); + ttbl.extend_from_slice(b"TTBL"); + ttbl.extend_from_slice(&ttbl_size.to_le_bytes()); + ttbl.extend_from_slice(&1u32.to_le_bytes()); // template count + + ttbl.extend_from_slice(b"TEMP"); + ttbl.extend_from_slice(&temp_size.to_le_bytes()); + ttbl.extend_from_slice(&0u32.to_le_bytes()); // item_descriptor_count + ttbl.extend_from_slice(&0u32.to_le_bytes()); // item_name_count + ttbl.extend_from_slice(&(ttbl_off + 12 + temp_size).to_le_bytes()); // template_items_offset (end-of-template) + ttbl.extend_from_slice(&1u32.to_le_bytes()); // event_type (observed as 1 for EventData) + ttbl.extend_from_slice(&guid_bytes); // template GUID + + let total_size = (ttbl_off as usize) + ttbl.len(); + let mut blob = Vec::with_capacity(total_size); + + // CRIM + blob.extend_from_slice(b"CRIM"); + blob.extend_from_slice(&(total_size as u32).to_le_bytes()); // size + blob.extend_from_slice(&3u16.to_le_bytes()); // major + blob.extend_from_slice(&1u16.to_le_bytes()); // minor + blob.extend_from_slice(&1u32.to_le_bytes()); // provider_count + + // provider descriptor + blob.extend_from_slice(&[0u8; 16]); // provider GUID (unused in this test) + blob.extend_from_slice(&provider_data_off.to_le_bytes()); + + // WEVT + blob.extend_from_slice(b"WEVT"); + blob.extend_from_slice(&(28u32).to_le_bytes()); // size + blob.extend_from_slice(&0xffffffffu32.to_le_bytes()); // message-table id + blob.extend_from_slice(&1u32.to_le_bytes()); // provider element desc count + blob.extend_from_slice(&0u32.to_le_bytes()); // unknown value count + // provider element descriptor + blob.extend_from_slice(&ttbl_off.to_le_bytes()); // provider element offset (relative to CRIM) + blob.extend_from_slice(&0u32.to_le_bytes()); // unknown + + // TTBL + blob.extend_from_slice(&ttbl); + + let temps = extract_temp_templates_from_wevt_blob(&blob).expect("parse should succeed"); + assert_eq!(temps.len(), 1); + let t = &temps[0]; + assert_eq!(t.ttbl_offset, ttbl_off); + assert_eq!(t.temp_offset, ttbl_off + 12); + assert_eq!(t.temp_size, temp_size); + assert_eq!(t.header.item_descriptor_count, 0); + assert_eq!(t.header.item_name_count, 0); + assert_eq!(t.header.template_items_offset, ttbl_off + 12 + temp_size); + assert_eq!(t.header.event_type, 1); + } + + #[test] + #[ignore] + fn it_extracts_wevt_template_from_willi_services_exe_sample() { + // This test is intentionally ignored by default: + // - it downloads a Windows binary (large, proprietary) + // - it requires network access / curl + // + // Run with: + // cargo test --features wevt_templates -- --ignored + + const URL: &str = "https://user-images.githubusercontent.com/156560/84550172-1e987a00-acc7-11ea-8f8e-7e1310b13ec4.gif"; + + // Prefer local developer copy if present. + let local_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("samples_local") + .join("services.exe.gif"); + + let bytes = if local_path.exists() { + fs::read(&local_path).unwrap() + } else { + let d = tempdir().unwrap(); + let p = d.path().join("services.exe.gif"); + + let status = Command::new("curl") + .args(["-L", "-s", "-o"]) + .arg(&p) + .arg(URL) + .status() + .expect("failed to spawn curl"); + assert!(status.success(), "curl failed"); + + fs::read(p).unwrap() + }; + + let resources = extract_wevt_template_resources(&bytes).expect("extract should succeed"); + assert!( + !resources.is_empty(), + "expected at least one WEVT_TEMPLATE resource" + ); + + // This particular sample has: WEVT_TEMPLATE / 1 / 1033 + let found = resources.iter().find(|r| r.lang_id == 1033); + assert!(found.is_some(), "expected lang_id=1033 resource"); + let r = found.unwrap(); + assert_eq!(r.resource, ResourceIdentifier::Id(1)); + assert!(r.data.starts_with(b"CRIM|K\0\0"), "expected CRIM header"); + assert!( + r.data.windows(4).any(|w| w == b"WEVT"), + "expected embedded WEVT marker" + ); + assert!( + r.data.windows(4).any(|w| w == b"TTBL"), + "expected embedded TTBL marker" + ); + assert!( + r.data.windows(4).any(|w| w == b"TEMP"), + "expected embedded TEMP marker" + ); + + let temps = extract_temp_templates_from_wevt_blob(&r.data).expect("parse should succeed"); + assert_eq!( + temps.len(), + 46, + "expected stable template count for Willi sample" + ); + + // Parse the full manifest and ensure every TEMP BinXML fragment parses cleanly with strict + // NameHash validation (MS-EVEN6) and the current token support. + let manifest = CrimManifest::parse(&r.data).expect("manifest parse should succeed"); + let mut parsed_templates = 0usize; + for provider in &manifest.providers { + if let Some(ttbl) = provider.wevt.elements.templates.as_ref() { + for tpl in &ttbl.templates { + let _ = parse_wevt_binxml_fragment(tpl.binxml, encoding::all::WINDOWS_1252) + .expect("BinXML parse should succeed"); + parsed_templates += 1; + } + } + } + assert!( + parsed_templates > 0, + "expected at least one parsed template" + ); + } +} diff --git a/wevt_cache_demo/index.jsonl b/wevt_cache_demo/index.jsonl new file mode 100644 index 00000000..52e308e5 --- /dev/null +++ b/wevt_cache_demo/index.jsonl @@ -0,0 +1,265 @@ +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"output_path":"wevt_cache_demo/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.bin","size":19326} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"0063715B-EEDA-4007-9429-AD526F62696E","event_index":0,"event_id":101,"version":0,"channel":16,"level":4,"opcode":1,"task":101,"keywords":9223653511831552000,"message_identifier":4294967295,"template_offset":null,"template_guid":null} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"0063715B-EEDA-4007-9429-AD526F62696E","event_index":1,"event_id":102,"version":0,"channel":16,"level":4,"opcode":2,"task":101,"keywords":9223653511831552000,"message_identifier":4294967295,"template_offset":null,"template_guid":null} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"0063715B-EEDA-4007-9429-AD526F62696E","event_index":2,"event_id":103,"version":0,"channel":16,"level":4,"opcode":1,"task":103,"keywords":9223372036854841344,"message_identifier":4294967295,"template_offset":276,"template_guid":"AB19BEFE-23F0-5F65-2FFD-444C0BE74F99"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"0063715B-EEDA-4007-9429-AD526F62696E","event_index":3,"event_id":104,"version":0,"channel":16,"level":4,"opcode":2,"task":103,"keywords":9223372036854841344,"message_identifier":4294967295,"template_offset":276,"template_guid":"AB19BEFE-23F0-5F65-2FFD-444C0BE74F99"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"0063715B-EEDA-4007-9429-AD526F62696E","event_index":4,"event_id":105,"version":0,"channel":16,"level":4,"opcode":101,"task":105,"keywords":9223372036854841344,"message_identifier":4294967295,"template_offset":468,"template_guid":"F46B32F6-74A3-5D9B-501F-ACF0B8C12A6D"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"0063715B-EEDA-4007-9429-AD526F62696E","event_index":5,"event_id":106,"version":0,"channel":16,"level":4,"opcode":1,"task":107,"keywords":9223653511831552000,"message_identifier":4294967295,"template_offset":null,"template_guid":null} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"0063715B-EEDA-4007-9429-AD526F62696E","event_index":6,"event_id":107,"version":0,"channel":16,"level":4,"opcode":2,"task":107,"keywords":9223653511831552000,"message_identifier":4294967295,"template_offset":null,"template_guid":null} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"06184C97-5201-480E-92AF-3A3626C5B140","event_index":0,"event_id":101,"version":0,"channel":16,"level":4,"opcode":1,"task":1,"keywords":9223372036854775808,"message_identifier":4294967295,"template_offset":2508,"template_guid":"7B1AB7EC-1B87-56FC-E029-B14B724CA75B"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"06184C97-5201-480E-92AF-3A3626C5B140","event_index":1,"event_id":102,"version":0,"channel":16,"level":4,"opcode":2,"task":1,"keywords":9223372036854775808,"message_identifier":4294967295,"template_offset":2508,"template_guid":"7B1AB7EC-1B87-56FC-E029-B14B724CA75B"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":0,"event_id":7000,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232472,"template_offset":3284,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":1,"event_id":7001,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232473,"template_offset":3696,"template_guid":"F24E48AD-D33B-5686-65EA-314655362F10"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":2,"event_id":7002,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232474,"template_offset":4208,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":3,"event_id":7003,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232475,"template_offset":4620,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":4,"event_id":7005,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232477,"template_offset":5032,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":5,"event_id":7006,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232478,"template_offset":5316,"template_guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":6,"event_id":7007,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232479,"template_offset":5700,"template_guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":7,"event_id":7008,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232480,"template_offset":5780,"template_guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":8,"event_id":7009,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232481,"template_offset":5860,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":9,"event_id":7010,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232482,"template_offset":6272,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":10,"event_id":7011,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232483,"template_offset":6452,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":11,"event_id":7012,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232484,"template_offset":6736,"template_guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":12,"event_id":7013,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232485,"template_offset":6816,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":13,"event_id":7014,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232486,"template_offset":6996,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":14,"event_id":7016,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232488,"template_offset":7176,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":15,"event_id":7017,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232489,"template_offset":7460,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":16,"event_id":7018,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232490,"template_offset":7768,"template_guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":17,"event_id":7019,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232491,"template_offset":7848,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":18,"event_id":7020,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232492,"template_offset":8156,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":19,"event_id":7021,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232493,"template_offset":8464,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":20,"event_id":7022,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232494,"template_offset":8644,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":21,"event_id":7023,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232495,"template_offset":8952,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":22,"event_id":7024,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232496,"template_offset":9364,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":23,"event_id":7026,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232498,"template_offset":9776,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":24,"event_id":7027,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232499,"template_offset":9956,"template_guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":25,"event_id":7028,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232500,"template_offset":10036,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":26,"event_id":7029,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232501,"template_offset":10216,"template_guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":27,"event_id":7030,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232502,"template_offset":10296,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":28,"event_id":7031,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232503,"template_offset":10476,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":29,"event_id":7032,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232504,"template_offset":11192,"template_guid":"BD652296-5283-5265-4C86-BFC0C8127598"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":30,"event_id":7034,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232506,"template_offset":11680,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":31,"event_id":7035,"version":0,"channel":0,"level":0,"opcode":64,"task":0,"keywords":36028797018963968,"message_identifier":1073748859,"template_offset":12092,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":32,"event_id":7036,"version":0,"channel":0,"level":0,"opcode":64,"task":0,"keywords":36028797018963968,"message_identifier":1073748860,"template_offset":12376,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":33,"event_id":7037,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232509,"template_offset":12788,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":34,"event_id":7038,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232510,"template_offset":13072,"template_guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":35,"event_id":7039,"version":0,"channel":0,"level":0,"opcode":128,"task":0,"keywords":36028797018963968,"message_identifier":2147490687,"template_offset":13456,"template_guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":36,"event_id":7040,"version":0,"channel":0,"level":0,"opcode":64,"task":0,"keywords":36028797018963968,"message_identifier":1073748864,"template_offset":13840,"template_guid":"BD652296-5283-5265-4C86-BFC0C8127598"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":37,"event_id":7041,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232513,"template_offset":14328,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":38,"event_id":7042,"version":0,"channel":0,"level":0,"opcode":64,"task":0,"keywords":36028797018963968,"message_identifier":1073748866,"template_offset":14612,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":39,"event_id":7043,"version":0,"channel":0,"level":0,"opcode":192,"task":0,"keywords":36028797018963968,"message_identifier":3221232515,"template_offset":15328,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":40,"event_id":7044,"version":0,"channel":0,"level":0,"opcode":128,"task":0,"keywords":36028797018963968,"message_identifier":2147490692,"template_offset":15636,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":41,"event_id":7045,"version":0,"channel":0,"level":0,"opcode":64,"task":0,"keywords":36028797018963968,"message_identifier":1073748869,"template_offset":16048,"template_guid":"B8477A50-EE65-5C9B-E393-55D7C6EFE60F"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"provider_guid":"555908D1-A6D7-4695-8E1E-26931D2012F4","event_index":42,"event_id":7046,"version":0,"channel":0,"level":0,"opcode":128,"task":0,"keywords":36028797018963968,"message_identifier":2147490694,"template_offset":16712,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":264,"template_offset":276,"template_guid":"AB19BEFE-23F0-5F65-2FFD-444C0BE74F99","item_index":0,"name":"GroupName","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":444,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":264,"template_offset":468,"template_guid":"F46B32F6-74A3-5D9B-501F-ACF0B8C12A6D","item_index":0,"name":"ExecutionPhase","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":1084,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":264,"template_offset":468,"template_guid":"F46B32F6-74A3-5D9B-501F-ACF0B8C12A6D","item_index":1,"name":"CurrentState","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":1120,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":264,"template_offset":468,"template_guid":"F46B32F6-74A3-5D9B-501F-ACF0B8C12A6D","item_index":2,"name":"StartType","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":1152,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":264,"template_offset":468,"template_guid":"F46B32F6-74A3-5D9B-501F-ACF0B8C12A6D","item_index":3,"name":"PID","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":1176,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":264,"template_offset":468,"template_guid":"F46B32F6-74A3-5D9B-501F-ACF0B8C12A6D","item_index":4,"name":"ServiceName","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":1188,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":264,"template_offset":468,"template_guid":"F46B32F6-74A3-5D9B-501F-ACF0B8C12A6D","item_index":5,"name":"ImageName","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":1216,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":2496,"template_offset":2508,"template_guid":"7B1AB7EC-1B87-56FC-E029-B14B724CA75B","item_index":0,"name":"ServiceName","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":2680,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":3284,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":3600,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":3284,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":3620,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":3284,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":2,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":3640,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":3284,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":3,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":2,"name_offset":3668,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":3696,"template_guid":"F24E48AD-D33B-5686-65EA-314655362F10","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":4092,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":3696,"template_guid":"F24E48AD-D33B-5686-65EA-314655362F10","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":4112,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":3696,"template_guid":"F24E48AD-D33B-5686-65EA-314655362F10","item_index":2,"name":"param3","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":4132,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":3696,"template_guid":"F24E48AD-D33B-5686-65EA-314655362F10","item_index":3,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":4152,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":3696,"template_guid":"F24E48AD-D33B-5686-65EA-314655362F10","item_index":4,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":3,"name_offset":4180,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":4208,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":4524,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":4208,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":4544,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":4208,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":2,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":4564,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":4208,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":3,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":2,"name_offset":4592,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":4620,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":4936,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":4620,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":4956,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":4620,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":2,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":4976,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":4620,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":3,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":2,"name_offset":5004,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":5032,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":5276,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":5032,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":5296,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":5316,"template_guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":5640,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":5316,"template_guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":5660,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":5316,"template_guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","item_index":2,"name":"param3","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":5680,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":5860,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":6176,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":5860,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":6196,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":5860,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":2,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":6216,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":5860,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":3,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":2,"name_offset":6244,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":6272,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":6432,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":6452,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":6696,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":6452,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":6716,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":6816,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":6976,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":6996,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":7156,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":7176,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":7420,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":7176,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":7440,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":7460,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":7692,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":7460,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":1,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":7712,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":7460,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":2,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":1,"name_offset":7740,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":7848,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":8080,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":7848,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":1,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":8100,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":7848,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":2,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":1,"name_offset":8128,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":8156,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":8388,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":8156,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":1,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":8408,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":8156,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":2,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":1,"name_offset":8436,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":8464,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":8624,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":8644,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":8876,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":8644,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":1,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":8896,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":8644,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":2,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":1,"name_offset":8924,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":8952,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":9268,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":8952,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":9288,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":8952,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":2,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":9308,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":8952,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":3,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":2,"name_offset":9336,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":9364,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":9680,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":9364,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":9700,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":9364,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":2,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":9720,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":9364,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":3,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":2,"name_offset":9748,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":9776,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":9936,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":10036,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":10196,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":10296,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":10456,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":10476,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":11036,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":10476,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":11056,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":10476,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":2,"name":"param3","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":11076,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":10476,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":3,"name":"param4","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":11096,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":10476,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":4,"name":"param5","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":11116,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":10476,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":5,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":11136,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":10476,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":6,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":5,"name_offset":11164,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":11192,"template_guid":"BD652296-5283-5265-4C86-BFC0C8127598","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":11600,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":11192,"template_guid":"BD652296-5283-5265-4C86-BFC0C8127598","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":11620,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":11192,"template_guid":"BD652296-5283-5265-4C86-BFC0C8127598","item_index":2,"name":"param3","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":11640,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":11192,"template_guid":"BD652296-5283-5265-4C86-BFC0C8127598","item_index":3,"name":"param4","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":11660,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":11680,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":11996,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":11680,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":12016,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":11680,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":2,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":12036,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":11680,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":3,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":2,"name_offset":12064,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":12092,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":12336,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":12092,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":12356,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":12376,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":12692,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":12376,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":12712,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":12376,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":2,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":12732,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":12376,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":3,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":2,"name_offset":12760,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":12788,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":13032,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":12788,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":13052,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":13072,"template_guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":13396,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":13072,"template_guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":13416,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":13072,"template_guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","item_index":2,"name":"param3","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":13436,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":13456,"template_guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":13780,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":13456,"template_guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":13800,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":13456,"template_guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","item_index":2,"name":"param3","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":13820,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":13840,"template_guid":"BD652296-5283-5265-4C86-BFC0C8127598","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":14248,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":13840,"template_guid":"BD652296-5283-5265-4C86-BFC0C8127598","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":14268,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":13840,"template_guid":"BD652296-5283-5265-4C86-BFC0C8127598","item_index":2,"name":"param3","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":14288,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":13840,"template_guid":"BD652296-5283-5265-4C86-BFC0C8127598","item_index":3,"name":"param4","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":14308,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":14328,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":14572,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":14328,"template_guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":14592,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":14612,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":15172,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":14612,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":15192,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":14612,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":2,"name":"param3","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":15212,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":14612,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":3,"name":"param4","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":15232,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":14612,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":4,"name":"param5","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":15252,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":14612,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":5,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":15272,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":14612,"template_guid":"BA1834BC-807C-559E-7050-940DA3B04A02","item_index":6,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":5,"name_offset":15300,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":15328,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":15560,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":15328,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":1,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":15580,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":15328,"template_guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","item_index":2,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":1,"name_offset":15608,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":15636,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":15952,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":15636,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":1,"name":"param2","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":15972,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":15636,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":2,"name":"__binLength","input_type":8,"output_type":8,"count":0,"length":0,"name_offset":15992,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":15636,"template_guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","item_index":3,"name":"BinaryData","input_type":14,"output_type":15,"count":0,"length":2,"name_offset":16020,"unknown1":4,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":16048,"template_guid":"B8477A50-EE65-5C9B-E393-55D7C6EFE60F","item_index":0,"name":"ServiceName","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":16580,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":16048,"template_guid":"B8477A50-EE65-5C9B-E393-55D7C6EFE60F","item_index":1,"name":"ImagePath","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":16608,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":16048,"template_guid":"B8477A50-EE65-5C9B-E393-55D7C6EFE60F","item_index":2,"name":"ServiceType","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":16632,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":16048,"template_guid":"B8477A50-EE65-5C9B-E393-55D7C6EFE60F","item_index":3,"name":"StartType","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":16660,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":16048,"template_guid":"B8477A50-EE65-5C9B-E393-55D7C6EFE60F","item_index":4,"name":"AccountName","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":16684,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"template_offset":16712,"template_guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","item_index":0,"name":"param1","input_type":1,"output_type":1,"count":0,"length":0,"name_offset":16872,"unknown1":0,"unknown3":0,"unknown4":0} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":264,"temp_offset":276,"temp_size":192,"item_descriptor_count":1,"item_name_count":1,"template_items_offset":424,"event_type":1,"guid":"AB19BEFE-23F0-5F65-2FFD-444C0BE74F99","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0000.AB19BEFE-23F0-5F65-2FFD-444C0BE74F99.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":0,"guid":"AB19BEFE-23F0-5F65-2FFD-444C0BE74F99","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0000.AB19BEFE-23F0-5F65-2FFD-444C0BE74F99.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":264,"temp_offset":468,"temp_size":772,"item_descriptor_count":6,"item_name_count":6,"template_items_offset":964,"event_type":1,"guid":"F46B32F6-74A3-5D9B-501F-ACF0B8C12A6D","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0001.F46B32F6-74A3-5D9B-501F-ACF0B8C12A6D.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":1,"guid":"F46B32F6-74A3-5D9B-501F-ACF0B8C12A6D","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0001.F46B32F6-74A3-5D9B-501F-ACF0B8C12A6D.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":2496,"temp_offset":2508,"temp_size":200,"item_descriptor_count":1,"item_name_count":1,"template_items_offset":2660,"event_type":1,"guid":"7B1AB7EC-1B87-56FC-E029-B14B724CA75B","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0002.7B1AB7EC-1B87-56FC-E029-B14B724CA75B.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":2,"guid":"7B1AB7EC-1B87-56FC-E029-B14B724CA75B","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0002.7B1AB7EC-1B87-56FC-E029-B14B724CA75B.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":3284,"temp_size":412,"item_descriptor_count":4,"item_name_count":4,"template_items_offset":3520,"event_type":1,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0003.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":3,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0003.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":3696,"temp_size":512,"item_descriptor_count":5,"item_name_count":5,"template_items_offset":3992,"event_type":1,"guid":"F24E48AD-D33B-5686-65EA-314655362F10","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0004.F24E48AD-D33B-5686-65EA-314655362F10.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":4,"guid":"F24E48AD-D33B-5686-65EA-314655362F10","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0004.F24E48AD-D33B-5686-65EA-314655362F10.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":4208,"temp_size":412,"item_descriptor_count":4,"item_name_count":4,"template_items_offset":4444,"event_type":1,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0005.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":5,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0005.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":4620,"temp_size":412,"item_descriptor_count":4,"item_name_count":4,"template_items_offset":4856,"event_type":1,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0006.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":6,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0006.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":5032,"temp_size":284,"item_descriptor_count":2,"item_name_count":2,"template_items_offset":5236,"event_type":1,"guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0007.F3037EBB-28FC-5E17-80BF-24556D3026EE.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":7,"guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0007.F3037EBB-28FC-5E17-80BF-24556D3026EE.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":5316,"temp_size":384,"item_descriptor_count":3,"item_name_count":3,"template_items_offset":5580,"event_type":1,"guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0008.74E08D9F-6F25-5E44-66BE-8B685BC2FD6C.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":8,"guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0008.74E08D9F-6F25-5E44-66BE-8B685BC2FD6C.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":5700,"temp_size":80,"item_descriptor_count":0,"item_name_count":0,"template_items_offset":5780,"event_type":1,"guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0009.3529151F-48E3-5E37-4183-BD98BB64AEC1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":9,"guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0009.3529151F-48E3-5E37-4183-BD98BB64AEC1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":5780,"temp_size":80,"item_descriptor_count":0,"item_name_count":0,"template_items_offset":5860,"event_type":1,"guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0010.3529151F-48E3-5E37-4183-BD98BB64AEC1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":10,"guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0010.3529151F-48E3-5E37-4183-BD98BB64AEC1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":5860,"temp_size":412,"item_descriptor_count":4,"item_name_count":4,"template_items_offset":6096,"event_type":1,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0011.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":11,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0011.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":6272,"temp_size":180,"item_descriptor_count":1,"item_name_count":1,"template_items_offset":6412,"event_type":1,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0012.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":12,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0012.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":6452,"temp_size":284,"item_descriptor_count":2,"item_name_count":2,"template_items_offset":6656,"event_type":1,"guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0013.F3037EBB-28FC-5E17-80BF-24556D3026EE.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":13,"guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0013.F3037EBB-28FC-5E17-80BF-24556D3026EE.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":6736,"temp_size":80,"item_descriptor_count":0,"item_name_count":0,"template_items_offset":6816,"event_type":1,"guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0014.3529151F-48E3-5E37-4183-BD98BB64AEC1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":14,"guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0014.3529151F-48E3-5E37-4183-BD98BB64AEC1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":6816,"temp_size":180,"item_descriptor_count":1,"item_name_count":1,"template_items_offset":6956,"event_type":1,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0015.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":15,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0015.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":6996,"temp_size":180,"item_descriptor_count":1,"item_name_count":1,"template_items_offset":7136,"event_type":1,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0016.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":16,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0016.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":7176,"temp_size":284,"item_descriptor_count":2,"item_name_count":2,"template_items_offset":7380,"event_type":1,"guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0017.F3037EBB-28FC-5E17-80BF-24556D3026EE.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":17,"guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0017.F3037EBB-28FC-5E17-80BF-24556D3026EE.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":7460,"temp_size":308,"item_descriptor_count":3,"item_name_count":3,"template_items_offset":7632,"event_type":1,"guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0018.76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":18,"guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0018.76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":7768,"temp_size":80,"item_descriptor_count":0,"item_name_count":0,"template_items_offset":7848,"event_type":1,"guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0019.3529151F-48E3-5E37-4183-BD98BB64AEC1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":19,"guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0019.3529151F-48E3-5E37-4183-BD98BB64AEC1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":7848,"temp_size":308,"item_descriptor_count":3,"item_name_count":3,"template_items_offset":8020,"event_type":1,"guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0020.76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":20,"guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0020.76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":8156,"temp_size":308,"item_descriptor_count":3,"item_name_count":3,"template_items_offset":8328,"event_type":1,"guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0021.76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":21,"guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0021.76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":8464,"temp_size":180,"item_descriptor_count":1,"item_name_count":1,"template_items_offset":8604,"event_type":1,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0022.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":22,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0022.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":8644,"temp_size":308,"item_descriptor_count":3,"item_name_count":3,"template_items_offset":8816,"event_type":1,"guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0023.76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":23,"guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0023.76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":8952,"temp_size":412,"item_descriptor_count":4,"item_name_count":4,"template_items_offset":9188,"event_type":1,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0024.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":24,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0024.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":9364,"temp_size":412,"item_descriptor_count":4,"item_name_count":4,"template_items_offset":9600,"event_type":1,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0025.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":25,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0025.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":9776,"temp_size":180,"item_descriptor_count":1,"item_name_count":1,"template_items_offset":9916,"event_type":1,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0026.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":26,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0026.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":9956,"temp_size":80,"item_descriptor_count":0,"item_name_count":0,"template_items_offset":10036,"event_type":1,"guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0027.3529151F-48E3-5E37-4183-BD98BB64AEC1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":27,"guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0027.3529151F-48E3-5E37-4183-BD98BB64AEC1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":10036,"temp_size":180,"item_descriptor_count":1,"item_name_count":1,"template_items_offset":10176,"event_type":1,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0028.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":28,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0028.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":10216,"temp_size":80,"item_descriptor_count":0,"item_name_count":0,"template_items_offset":10296,"event_type":1,"guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0029.3529151F-48E3-5E37-4183-BD98BB64AEC1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":29,"guid":"3529151F-48E3-5E37-4183-BD98BB64AEC1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0029.3529151F-48E3-5E37-4183-BD98BB64AEC1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":10296,"temp_size":180,"item_descriptor_count":1,"item_name_count":1,"template_items_offset":10436,"event_type":1,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0030.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":30,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0030.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":10476,"temp_size":716,"item_descriptor_count":7,"item_name_count":7,"template_items_offset":10896,"event_type":1,"guid":"BA1834BC-807C-559E-7050-940DA3B04A02","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0031.BA1834BC-807C-559E-7050-940DA3B04A02.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":31,"guid":"BA1834BC-807C-559E-7050-940DA3B04A02","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0031.BA1834BC-807C-559E-7050-940DA3B04A02.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":11192,"temp_size":488,"item_descriptor_count":4,"item_name_count":4,"template_items_offset":11520,"event_type":1,"guid":"BD652296-5283-5265-4C86-BFC0C8127598","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0032.BD652296-5283-5265-4C86-BFC0C8127598.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":32,"guid":"BD652296-5283-5265-4C86-BFC0C8127598","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0032.BD652296-5283-5265-4C86-BFC0C8127598.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":11680,"temp_size":412,"item_descriptor_count":4,"item_name_count":4,"template_items_offset":11916,"event_type":1,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0033.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":33,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0033.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":12092,"temp_size":284,"item_descriptor_count":2,"item_name_count":2,"template_items_offset":12296,"event_type":1,"guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0034.F3037EBB-28FC-5E17-80BF-24556D3026EE.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":34,"guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0034.F3037EBB-28FC-5E17-80BF-24556D3026EE.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":12376,"temp_size":412,"item_descriptor_count":4,"item_name_count":4,"template_items_offset":12612,"event_type":1,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0035.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":35,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0035.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":12788,"temp_size":284,"item_descriptor_count":2,"item_name_count":2,"template_items_offset":12992,"event_type":1,"guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0036.F3037EBB-28FC-5E17-80BF-24556D3026EE.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":36,"guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0036.F3037EBB-28FC-5E17-80BF-24556D3026EE.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":13072,"temp_size":384,"item_descriptor_count":3,"item_name_count":3,"template_items_offset":13336,"event_type":1,"guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0037.74E08D9F-6F25-5E44-66BE-8B685BC2FD6C.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":37,"guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0037.74E08D9F-6F25-5E44-66BE-8B685BC2FD6C.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":13456,"temp_size":384,"item_descriptor_count":3,"item_name_count":3,"template_items_offset":13720,"event_type":1,"guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0038.74E08D9F-6F25-5E44-66BE-8B685BC2FD6C.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":38,"guid":"74E08D9F-6F25-5E44-66BE-8B685BC2FD6C","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0038.74E08D9F-6F25-5E44-66BE-8B685BC2FD6C.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":13840,"temp_size":488,"item_descriptor_count":4,"item_name_count":4,"template_items_offset":14168,"event_type":1,"guid":"BD652296-5283-5265-4C86-BFC0C8127598","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0039.BD652296-5283-5265-4C86-BFC0C8127598.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":39,"guid":"BD652296-5283-5265-4C86-BFC0C8127598","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0039.BD652296-5283-5265-4C86-BFC0C8127598.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":14328,"temp_size":284,"item_descriptor_count":2,"item_name_count":2,"template_items_offset":14532,"event_type":1,"guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0040.F3037EBB-28FC-5E17-80BF-24556D3026EE.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":40,"guid":"F3037EBB-28FC-5E17-80BF-24556D3026EE","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0040.F3037EBB-28FC-5E17-80BF-24556D3026EE.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":14612,"temp_size":716,"item_descriptor_count":7,"item_name_count":7,"template_items_offset":15032,"event_type":1,"guid":"BA1834BC-807C-559E-7050-940DA3B04A02","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0041.BA1834BC-807C-559E-7050-940DA3B04A02.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":41,"guid":"BA1834BC-807C-559E-7050-940DA3B04A02","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0041.BA1834BC-807C-559E-7050-940DA3B04A02.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":15328,"temp_size":308,"item_descriptor_count":3,"item_name_count":3,"template_items_offset":15500,"event_type":1,"guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0042.76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":42,"guid":"76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0042.76DC3CB2-EEFB-5474-C923-D8DF7EDCB2D5.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":15636,"temp_size":412,"item_descriptor_count":4,"item_name_count":4,"template_items_offset":15872,"event_type":1,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0043.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":43,"guid":"5EB89DDA-8F78-57E7-0A26-7641ACF8B32E","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0043.5EB89DDA-8F78-57E7-0A26-7641ACF8B32E.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":16048,"temp_size":664,"item_descriptor_count":5,"item_name_count":5,"template_items_offset":16480,"event_type":1,"guid":"B8477A50-EE65-5C9B-E393-55D7C6EFE60F","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0044.B8477A50-EE65-5C9B-E393-55D7C6EFE60F.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":44,"guid":"B8477A50-EE65-5C9B-E393-55D7C6EFE60F","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0044.B8477A50-EE65-5C9B-E393-55D7C6EFE60F.xml"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"ttbl_offset":3272,"temp_offset":16712,"temp_size":180,"item_descriptor_count":1,"item_name_count":1,"template_items_offset":16852,"event_type":1,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0045.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.bin"} +{"source":"samples_local/services.exe.gif","resource":1,"lang_id":1033,"temp_index":45,"guid":"E928EE6F-E5F4-5039-F097-86A4CEABDBE1","output_path":"wevt_cache_demo/temp_xml/services.exe.gif.decacb30.wevt_template.id_1.lang_1033.temp_0045.E928EE6F-E5F4-5039-F097-86A4CEABDBE1.xml"}