From f46f723fa37f6ea046c6597443acf756fc75d374 Mon Sep 17 00:00:00 2001 From: David Magnotti <78613347+davidmagnotti@users.noreply.github.com> Date: Sun, 12 Jan 2025 19:34:40 -0500 Subject: [PATCH 01/12] feat: OLE CF and VBA Modules Added - Added support for parsing OLE CF and VBA (macro-enabled Office) files. --- cli/src/commands/dump.rs | 14 + lib/Cargo.toml | 9 + lib/src/modules/add_modules.rs | 4 + lib/src/modules/mod.rs | 20 + lib/src/modules/modules.rs | 6 +- lib/src/modules/olecf/mod.rs | 53 ++ lib/src/modules/olecf/parser.rs | 400 +++++++++++ ...224f0d3a780f44fbb04fcf7caae34b973eb766.out | 16 + ...224f0d3a780f44fbb04fcf7caae34b973eb766.zip | Bin 0 -> 12956 bytes ...6e16cc04f43f53935a885c99c21148c975a705.out | 2 + ...6e16cc04f43f53935a885c99c21148c975a705.zip | Bin 0 -> 688 bytes lib/src/modules/protos/mods.proto | 4 + lib/src/modules/protos/olecf.proto | 22 + lib/src/modules/protos/vba.proto | 37 + lib/src/modules/vba/mod.rs | 207 ++++++ lib/src/modules/vba/parser.rs | 662 ++++++++++++++++++ ...ac3b0aaa284c91c47c26cfc2dbb3bc7f569103.out | 17 + ...ac3b0aaa284c91c47c26cfc2dbb3bc7f569103.zip | Bin 0 -> 22055 bytes ...224f0d3a780f44fbb04fcf7caae34b973eb766.out | 2 + ...224f0d3a780f44fbb04fcf7caae34b973eb766.zip | Bin 0 -> 12956 bytes ...63f65da037c4a6f7ec0112832b95916ac8a1fb.out | 17 + ...63f65da037c4a6f7ec0112832b95916ac8a1fb.zip | Bin 0 -> 17631 bytes 22 files changed, 1491 insertions(+), 1 deletion(-) create mode 100644 lib/src/modules/olecf/mod.rs create mode 100644 lib/src/modules/olecf/parser.rs create mode 100644 lib/src/modules/olecf/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.out create mode 100644 lib/src/modules/olecf/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.zip create mode 100644 lib/src/modules/olecf/tests/testdata/cc354533e3a8190985784e476d6e16cc04f43f53935a885c99c21148c975a705.out create mode 100644 lib/src/modules/olecf/tests/testdata/cc354533e3a8190985784e476d6e16cc04f43f53935a885c99c21148c975a705.zip create mode 100644 lib/src/modules/protos/olecf.proto create mode 100644 lib/src/modules/protos/vba.proto create mode 100644 lib/src/modules/vba/mod.rs create mode 100644 lib/src/modules/vba/parser.rs create mode 100644 lib/src/modules/vba/tests/testdata/643d1e3b68c1e31aef5779eb28ac3b0aaa284c91c47c26cfc2dbb3bc7f569103.out create mode 100644 lib/src/modules/vba/tests/testdata/643d1e3b68c1e31aef5779eb28ac3b0aaa284c91c47c26cfc2dbb3bc7f569103.zip create mode 100644 lib/src/modules/vba/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.out create mode 100644 lib/src/modules/vba/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.zip create mode 100644 lib/src/modules/vba/tests/testdata/c62c12501055319db152f092e263f65da037c4a6f7ec0112832b95916ac8a1fb.out create mode 100644 lib/src/modules/vba/tests/testdata/c62c12501055319db152f092e263f65da037c4a6f7ec0112832b95916ac8a1fb.zip diff --git a/cli/src/commands/dump.rs b/cli/src/commands/dump.rs index c150b7b3c..cd0794ef3 100644 --- a/cli/src/commands/dump.rs +++ b/cli/src/commands/dump.rs @@ -20,6 +20,8 @@ enum SupportedModules { Elf, Pe, Dotnet, + Olecf, + Vba } #[derive(Debug, Clone, ValueEnum)] @@ -111,6 +113,12 @@ pub fn exec_dump(args: &ArgMatches) -> anyhow::Result<()> { if !requested_modules.contains(&&SupportedModules::Pe) { module_output.pe = MessageField::none() } + if !requested_modules.contains(&&SupportedModules::Olecf) { + module_output.olecf = MessageField::none() + } + if !requested_modules.contains(&&SupportedModules::Vba) { + module_output.vba = MessageField::none() + } } else { // Module was not specified, only show those that produced meaningful // results, the rest are cleared out. @@ -131,6 +139,12 @@ pub fn exec_dump(args: &ArgMatches) -> anyhow::Result<()> { if !module_output.pe.is_pe() { module_output.pe = MessageField::none() } + if !module_output.olecf.is_olecf() { + module_output.olecf = MessageField::none() + } + if !module_output.vba.has_macros() { + module_output.vba = MessageField::none() + } } match output_format { diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 4e112e8cf..a9260313b 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -146,6 +146,9 @@ magic-module = [ # The `math` module. math-module = [] +# The `olecf` module +olecf-module = [] + # The `pe` module parses PE files. pe-module = [ "dep:const-oid", @@ -182,6 +185,9 @@ text-module = [ # conditions of a rule to check against other epoch time. time-module = [] +# The `vba` module +vba-module = [] + # Features that are enabled by default. default = [ "constant-folding", @@ -194,10 +200,12 @@ default = [ "macho-module", "math-module", "hash-module", + "olecf-module", "pe-module", "string-module", "time-module", "lnk-module", + "vba-module", "test_proto2-module", "test_proto3-module", ] @@ -260,6 +268,7 @@ x509-parser = { workspace = true, optional = true } yansi = { workspace = true } yara-x-macros = { workspace = true } yara-x-parser = { workspace = true, features = ["serde"] } +zip = { workspace = true } lingua = { version = "1.6.2", optional = true, default-features = false, features = ["english", "german", "french", "spanish"] } diff --git a/lib/src/modules/add_modules.rs b/lib/src/modules/add_modules.rs index a9a528088..9d9fdd56d 100644 --- a/lib/src/modules/add_modules.rs +++ b/lib/src/modules/add_modules.rs @@ -18,6 +18,8 @@ add_module!(modules, "macho", macho, "macho.Macho", Some("macho"), Some(macho::_ add_module!(modules, "magic", magic, "magic.Magic", Some("magic"), Some(magic::__main__ as MainFn)); #[cfg(feature = "math-module")] add_module!(modules, "math", math, "math.Math", Some("math"), Some(math::__main__ as MainFn)); +#[cfg(feature = "olecf-module")] +add_module!(modules, "olecf", olecf, "olecf.Olecf", Some("olecf"), Some(olecf::__main__ as MainFn)); #[cfg(feature = "pe-module")] add_module!(modules, "pe", pe, "pe.PE", Some("pe"), Some(pe::__main__ as MainFn)); #[cfg(feature = "string-module")] @@ -30,4 +32,6 @@ add_module!(modules, "test_proto3", test_proto3, "test_proto3.TestProto3", Some( add_module!(modules, "text", text, "text.Text", Some("text"), Some(text::__main__ as MainFn)); #[cfg(feature = "time-module")] add_module!(modules, "time", time, "time.Time", Some("time"), Some(time::__main__ as MainFn)); +#[cfg(feature = "vba-module")] +add_module!(modules, "vba", vba, "vba.Vba", Some("vba"), Some(vba::__main__ as MainFn)); } \ No newline at end of file diff --git a/lib/src/modules/mod.rs b/lib/src/modules/mod.rs index d776d48b7..7b2f83895 100644 --- a/lib/src/modules/mod.rs +++ b/lib/src/modules/mod.rs @@ -174,6 +174,24 @@ pub mod mods { /// Data structure returned by the `macho` module. pub use super::protos::macho::Macho; + /// Data structures defined by the `olecf` module. + /// + /// The main structure produced by the module is [`olecf:Olecf`]. The rest + /// of them are used by one or more fields in the main structure. + /// + pub use super::protos::olecf; + /// Data structure returned by the `olecf` module. + pub use super::protos::olecf::Olecf; + + /// Data structures defined by the `vba` module. + /// + /// The main structure produced by the module is [`vba::Vba`]. The rest + /// of them are used by one or more fields in the main structure. + /// + pub use super::protos::vba; + /// Data structure returned by the `macho` module. + pub use super::protos::vba::Vba; + /// Data structures defined by the `pe` module. /// /// The main structure produced by the module is [`pe::PE`]. The rest @@ -268,6 +286,8 @@ pub mod mods { info.dotnet = protobuf::MessageField(invoke::(data)); info.macho = protobuf::MessageField(invoke::(data)); info.lnk = protobuf::MessageField(invoke::(data)); + info.olecf = protobuf::MessageField(invoke::(data)); + info.vba = protobuf::MessageField(invoke::(data)); info } diff --git a/lib/src/modules/modules.rs b/lib/src/modules/modules.rs index 7113eeaa0..e75a4c52a 100644 --- a/lib/src/modules/modules.rs +++ b/lib/src/modules/modules.rs @@ -17,6 +17,8 @@ mod macho; mod magic; #[cfg(feature = "math-module")] mod math; +#[cfg(feature = "olecf-module")] +mod olecf; #[cfg(feature = "pe-module")] mod pe; #[cfg(feature = "string-module")] @@ -28,4 +30,6 @@ mod test_proto3; #[cfg(feature = "text-module")] mod text; #[cfg(feature = "time-module")] -mod time; \ No newline at end of file +mod time; +#[cfg(feature = "vba-module")] +mod vba; \ No newline at end of file diff --git a/lib/src/modules/olecf/mod.rs b/lib/src/modules/olecf/mod.rs new file mode 100644 index 000000000..18d18dadf --- /dev/null +++ b/lib/src/modules/olecf/mod.rs @@ -0,0 +1,53 @@ +/*! YARA module that parses OLE Compound File Binary Format files. + +The OLE CF format (also known as Compound File Binary Format or CFBF) is a +container format used by many Microsoft file formats including DOC, XLS, PPT, +and MSI. This module specializes in parsing OLE CF files and extracting +metadata about their structure and contents. + +Read more about the Compound File Binary File format here: + https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b +*/ + +use crate::modules::prelude::*; +use crate::modules::protos::olecf::*; +pub mod parser; + +#[module_main] +fn main(data: &[u8], _meta: Option<&[u8]>) -> Olecf { + + match parser::OLECFParser::new(data) { + Ok(parser) => { + let mut olecf = Olecf::new(); + + // Check and set is_olecf + let is_valid = parser.is_valid_header(); + olecf.is_olecf = Some(is_valid); + + // Get stream names and sizes + match parser.get_stream_names() { + Ok(names) => { + // Get sizes for each stream + olecf.stream_sizes = names.iter() + .filter_map(|name| { + parser.get_stream_size(name) + .ok() + .map(|size| size as i64) + }) + .collect(); + + // Assign names last after we're done using them + olecf.stream_names = names; + }, + Err(_) => (), + } + + olecf + }, + Err(_) => { + let mut olecf = Olecf::new(); + olecf.is_olecf = Some(false); + olecf + } + } +} \ No newline at end of file diff --git a/lib/src/modules/olecf/parser.rs b/lib/src/modules/olecf/parser.rs new file mode 100644 index 000000000..56a7ca705 --- /dev/null +++ b/lib/src/modules/olecf/parser.rs @@ -0,0 +1,400 @@ +use std::collections::HashMap; +use nom::{ + bytes::complete::take, + combinator::verify, + error::{Error as NomError, ErrorKind}, + multi::count, + number::complete::{le_u16, le_u32}, + sequence::tuple, + IResult, +}; + +const OLECF_SIGNATURE: &[u8] = &[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]; +const SECTOR_SHIFT: u16 = 9; +const MINI_SECTOR_SHIFT: u16 = 6; +const DIRECTORY_ENTRY_SIZE: u64 = 128; + +// Directory Entry Types +const STORAGE_TYPE: u8 = 1; +const STREAM_TYPE: u8 = 2; +const ROOT_STORAGE_TYPE: u8 = 5; + +// Special sectors +const ENDOFCHAIN: u32 = 0xFFFFFFFE; +const FREESECT: u32 = 0xFFFFFFFF; +const MAX_REGULAR_SECTOR: u32 = 0xFFFFFFFA; + +pub struct OLECFParser<'a> { + data: &'a [u8], + sector_size: usize, + mini_sector_size: usize, + fat_sectors: Vec, + directory_sectors: Vec, + mini_fat_sectors: Vec, + dir_entries: HashMap, + mini_stream_start: u32, + mini_stream_size: u64, +} + +struct DirectoryEntry { + name: String, + size: u64, + start_sector: u32, + stream_type: u8, +} + +impl<'a> OLECFParser<'a> { + pub fn new(data: &'a [u8]) -> Result { + let mut parser = OLECFParser { + data, + sector_size: 1 << SECTOR_SHIFT, + mini_sector_size: 1 << MINI_SECTOR_SHIFT, + fat_sectors: Vec::new(), + directory_sectors: Vec::new(), + mini_fat_sectors: Vec::new(), + dir_entries: HashMap::new(), + mini_stream_start: 0, + mini_stream_size: 0, + }; + + match parser.parse(data) { + Ok((_rest, ())) => Ok(parser), + Err(_) => Err("Failed to parse OLECF data"), + } + } + + fn parse(&mut self, input: &'a [u8]) -> IResult<&'a [u8], ()> { + // (A) Check the 8-byte OLECF signature. + let (input, _) = verify(take(8_usize), |sig: &[u8]| sig == OLECF_SIGNATURE)(input)?; + + // (B) Parse the rest of the header fields. + let (input, ()) = self.parse_header(input)?; + + // (C) Parse the directory chain. + let (input, ()) = self.parse_directory(input)?; + + Ok((input, ())) + } + + + fn parse_header(&mut self, input: &'a [u8]) -> IResult<&'a [u8], ()> { + let (mut input, ( + _skip_20, + byte_order, + _skip_14, + num_fat_sectors, + first_dir_sector, + _skip_8, + first_mini_fat, + mini_fat_count, + _first_difat_sector, + _difat_count, + )) = tuple(( + take(20usize), // skip 20 bytes + le_u16, // parse byte_order + take(14usize), // skip 14 bytes + le_u32, // parse num_fat_sectors + le_u32, // parse first_dir_sector + take(8usize), // skip 8 bytes + le_u32, // parse first_mini_fat + le_u32, // parse mini_fat_count + le_u32, // parse _first_difat_sector + le_u32, // parse _difat_count + ))(input)?; + + // (A) Verify `byte_order == 0xFFFE`. + if byte_order != 0xFFFE { + return Err(nom::Err::Error(NomError::new(input, ErrorKind::Verify))); + } + + // (B) Parse up to 109 DIFAT entries from `input` + // 109 is the max allowed number of DIFAT entries in the header. + let rest = input; + if rest.len() < 109 * 4 { + let possible = rest.len() / 4; + let (rest2, entries) = count(le_u32, possible)(rest)?; + let mut filtered = entries + .into_iter() + .filter(|&x| x < MAX_REGULAR_SECTOR) + .collect::>(); + self.fat_sectors.append(&mut filtered); + input = rest2; + } else { + let (rest2, entries) = count(le_u32, 109)(rest)?; + let mut filtered = entries + .into_iter() + .filter(|&x| x < MAX_REGULAR_SECTOR) + .collect::>(); + self.fat_sectors.append(&mut filtered); + input = rest2; + } + + // (C) Directory chain + if first_dir_sector < MAX_REGULAR_SECTOR { + self.directory_sectors = self.follow_chain(first_dir_sector); + } else { + return Err(nom::Err::Error(NomError::new(input, ErrorKind::Verify))); + } + + // (D) MiniFAT chain + if mini_fat_count > 0 && first_mini_fat < MAX_REGULAR_SECTOR { + self.mini_fat_sectors = self.follow_chain(first_mini_fat); + } + + // (E) If no FAT sectors but num_fat_sectors != 0 => error + if self.fat_sectors.is_empty() && num_fat_sectors > 0 { + return Err(nom::Err::Error(NomError::new(input, ErrorKind::Verify))); + } + + Ok((input, ())) + } + + + fn parse_directory(&mut self, _input: &'a [u8]) -> IResult<&'a [u8], ()> { + if self.directory_sectors.is_empty() { + return Err(nom::Err::Error(NomError::new(_input, ErrorKind::Verify))); + } + + for §or in &self.directory_sectors { + let mut entry_offset = 0; + + while entry_offset + DIRECTORY_ENTRY_SIZE as usize <= self.sector_size { + let abs_offset = self.sector_to_offset(sector) + entry_offset; + if abs_offset + DIRECTORY_ENTRY_SIZE as usize > self.data.len() { + break; + } + match self.read_directory_entry(abs_offset) { + Ok(entry) => { + if entry.stream_type == ROOT_STORAGE_TYPE { + self.mini_stream_start = entry.start_sector; + self.mini_stream_size = entry.size; + } + if entry.stream_type == STORAGE_TYPE + || entry.stream_type == STREAM_TYPE + || entry.stream_type == ROOT_STORAGE_TYPE + { + self.dir_entries.insert(entry.name.clone(), entry); + } + } + Err(_) => {} + } + entry_offset += DIRECTORY_ENTRY_SIZE as usize; + } + } + + Ok((_input, ())) + } + + pub fn is_valid_header(&self) -> bool { + self.data.len() >= OLECF_SIGNATURE.len() + && &self.data[..OLECF_SIGNATURE.len()] == OLECF_SIGNATURE + } + + pub fn get_stream_names(&self) -> Result, &'static str> { + if self.dir_entries.is_empty() { + return Err("No streams found"); + } + Ok(self.dir_entries.keys().cloned().collect()) + } + + pub fn get_stream_size(&self, stream_name: &str) -> Result { + self.dir_entries.get(stream_name).map(|e| e.size).ok_or("Stream not found") + } + + pub fn get_stream_data(&self, stream_name: &str) -> Result, &'static str> { + let entry = self.dir_entries + .get(stream_name) + .ok_or("Stream not found")?; + + if entry.size < 4096 && entry.stream_type != ROOT_STORAGE_TYPE { + self.get_mini_stream_data(entry.start_sector, entry.size) + } else { + self.get_regular_stream_data(entry.start_sector, entry.size) + } + } + + fn sector_to_offset(&self, sector: u32) -> usize { + // The first sector begins at offset 512 + 512 + (sector as usize * self.sector_size) + } + + fn read_sector(&self, sector: u32) -> Result<&[u8], &'static str> { + let offset = self.sector_to_offset(sector); + if offset + self.sector_size > self.data.len() { + return Err("Sector read out of bounds"); + } + Ok(&self.data[offset..offset + self.sector_size]) + } + + fn get_fat_entry(&self, sector: u32) -> Result { + let entry_index = sector as usize; + let entries_per_sector = self.sector_size / 4; + let fat_sector_index = entry_index / entries_per_sector; + if fat_sector_index >= self.fat_sectors.len() { + return Err("FAT entry sector index out of range"); + } + let fat_sector = self.fat_sectors[fat_sector_index]; + let fat = self.read_sector(fat_sector)?; + let fat_entry_offset = (entry_index % entries_per_sector) * 4; + parse_u32_at(fat, fat_entry_offset) + } + + fn follow_chain(&self, start_sector: u32) -> Vec { + let mut chain = Vec::new(); + if start_sector >= MAX_REGULAR_SECTOR { + return chain; + } + + let mut current = start_sector; + while current < MAX_REGULAR_SECTOR { + chain.push(current); + let next = match self.get_fat_entry(current) { + Ok(n) => n, + Err(_) => break, + }; + if next >= MAX_REGULAR_SECTOR || next == FREESECT || next == ENDOFCHAIN { + break; + } + current = next; + } + chain + } + + fn read_directory_entry(&self, offset: usize) -> Result { + if offset + 128 > self.data.len() { + return Err("Incomplete directory entry"); + } + + let name_len = parse_u16_at(self.data, offset + 64)? as usize; + if name_len < 2 || name_len > 64 { + return Err("Invalid name length"); + } + + let name_bytes = &self.data[offset..offset + name_len]; + let filtered: Vec = name_bytes.iter().copied().filter(|&b| b != 0).collect(); + let name = String::from_utf8_lossy(&filtered).to_string(); + + let stream_type = self.data[offset + 66]; + let start_sector = parse_u32_at(self.data, offset + 116)?; + let size_32 = parse_u32_at(self.data, offset + 120)?; + let size = size_32 as u64; + + Ok(DirectoryEntry { + name, + size, + start_sector, + stream_type, + }) + } + + fn get_regular_stream_data(&self, start_sector: u32, size: u64) -> Result, &'static str> { + let mut data = Vec::with_capacity(size as usize); + let mut current_sector = start_sector; + let mut total_read = 0; + + while current_sector < MAX_REGULAR_SECTOR && total_read < size as usize { + let sector_data = self.read_sector(current_sector)?; + let bytes_to_read = std::cmp::min(self.sector_size, size as usize - total_read); + + data.extend_from_slice(§or_data[..bytes_to_read]); + total_read += bytes_to_read; + + if total_read < size as usize { + let next = self.get_fat_entry(current_sector)?; + if next == ENDOFCHAIN || next >= MAX_REGULAR_SECTOR { + break; + } + current_sector = next; + } + } + + if data.len() != size as usize { + return Err("Incomplete stream data"); + } + + Ok(data) + } + + fn get_root_mini_stream_data(&self) -> Result, &'static str> { + self.get_regular_stream_data(self.mini_stream_start, self.mini_stream_size) + } + + fn get_minifat_entry(&self, mini_sector: u32) -> Result { + if self.mini_fat_sectors.is_empty() { + return Ok(ENDOFCHAIN); + } + + let entry_index = mini_sector as usize; + let entries_per_sector = self.sector_size / 4; + let fat_sector_index = entry_index / entries_per_sector; + if fat_sector_index >= self.mini_fat_sectors.len() { + return Ok(ENDOFCHAIN); + } + let sector = self.mini_fat_sectors[fat_sector_index]; + let fat = self.read_sector(sector)?; + let offset = (entry_index % entries_per_sector) * 4; + parse_u32_at(fat, offset) + } + + fn get_mini_stream_data(&self, start_mini_sector: u32, size: u64) -> Result, &'static str> { + if self.mini_stream_size == 0 { + return Err("No mini stream present"); + } + + let mini_stream_data = self.get_root_mini_stream_data()?; + let mini_data_len = mini_stream_data.len(); + + let mut data = Vec::with_capacity(size as usize); + let mut current = start_mini_sector; + + while current < MAX_REGULAR_SECTOR && data.len() < size as usize { + let mini_offset = current as usize * self.mini_sector_size; + if mini_offset >= mini_data_len { + return Err("Mini stream offset out of range"); + } + + let bytes_to_read = std::cmp::min(self.mini_sector_size, size as usize - data.len()); + if mini_offset + bytes_to_read > mini_data_len { + return Err("Mini stream extends beyond available data"); + } + + data.extend_from_slice(&mini_stream_data[mini_offset..mini_offset + bytes_to_read]); + + if data.len() < size as usize { + let next = self.get_minifat_entry(current)?; + if next == ENDOFCHAIN || next >= MAX_REGULAR_SECTOR { + break; + } + current = next; + } + } + + if data.len() != size as usize { + return Err("Incomplete mini stream data"); + } + + Ok(data) + } +} + +fn parse_u16_at(data: &[u8], offset: usize) -> Result { + if offset + 2 > data.len() { + return Err("Buffer too small for u16"); + } + let slice = &data[offset..offset + 2]; + match le_u16::<&[u8], NomError<&[u8]>>(slice) { + Ok((_, val)) => Ok(val), + Err(_) => Err("Failed to parse u16"), + } +} + +fn parse_u32_at(data: &[u8], offset: usize) -> Result { + if offset + 4 > data.len() { + return Err("Buffer too small for u32"); + } + let slice = &data[offset..offset + 4]; + match le_u32::<&[u8], NomError<&[u8]>>(slice) { + Ok((_, val)) => Ok(val), + Err(_) => Err("Failed to parse u32"), + } +} \ No newline at end of file diff --git a/lib/src/modules/olecf/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.out b/lib/src/modules/olecf/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.out new file mode 100644 index 000000000..e1947b31e --- /dev/null +++ b/lib/src/modules/olecf/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.out @@ -0,0 +1,16 @@ +olecf: + is_olecf: true + stream_names: + - "CompObj" + - "1Table" + - "SummaryInformation" + - "Root Entry" + - "WordDocument" + - "DocumentSummaryInformation" + stream_sizes: + - 114 + - 7273 + - 4096 + - 128 + - 4096 + - 4096 \ No newline at end of file diff --git a/lib/src/modules/olecf/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.zip b/lib/src/modules/olecf/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.zip new file mode 100644 index 0000000000000000000000000000000000000000..2236f51f39bfdcd1705e11d9cd47f61a3199e85a GIT binary patch literal 12956 zcmdUWcUTkK+OLQRNS7{6bR#Ie2LeP;K}Em{2uKwoAWb@i1TcUoMNlbHB2ra)?=1qG z8jAE1AVNf1AV7$OVP>sav)=x$_5S9zA^i!SV@$vY{PNwy zV}JdVJv?^o%CWP@K#v_292M;C?LaDyN=_gJ2PH=(dsUFVnwpZ5ij%@)Wjl3{f|H7h zlfAuyij#wrx`Umaqq2&fc{`p(`oby-r zdFHPNFnM|ge72{MpMjVNH$bNdx*%qPF-ZEfMB$G>;Hcv^-D%`B-I-yM4k||SYV)_h z%v83!hKVlyh`(+=@@)6&hB!c*+=7v^5(1*{={2X2O+^;NgXOTAy6!{bz zz#Hq)RX+t;T8la){_5uuF^)$x-j=v$ffcSk)#>fGZ7^l`DXxC|_xB7nn6kzs9~e1o z-t*8vw%7?hdD@*8y6=KaMqGwHXg^F&tVu?pyXF16R~es(O?kEa+J-rb+2NKw8sYn7 ze+>I3qSeV9bnC0UQLwa^X>9Vr4o5u2dV3Y?>>mJwrA#CR7g(b#eQV;{w<&~*?;*Y^ zm)ISr(hPc&VfO4M5&_`+ABdKa{Vjhl#CZw20i<$E6H?u%iE8AK*~1@z!xlijwj`-z z)$mq%1ZuA6F8BO%!nszk-Os?4k^7M9Ai+$Py#Dkv?i(SwyV*JZ5mxdRIhERQMAOY` zIre)Plo81kVTw!3v}tKPt<&s*NY0Gt_zpjv{@lEvP^2r2G&^` zo9<`V+4Vu}pWFO|m42u0A3T_WEj&Y85cMlg%o+C_eG&4!N<)0!^a(ysXQBd&?K@K@!{8eleX=7nd9B<}zh zP|T8ab4*Y=Yi2|0NxK_BH*M}DpaG!upHILr)y^=_@2#|L3TYKFP1}ere=zLr`>#9y zn_<|c@lgUZ|6=fe)Bi6MT2uG}Q}rh}>^RiNr7L6eGb<406!XK0Vh2g#l4m6x4Ps^x z_wj^R#}B>{H^fBYT39ntLG*@qQ?S@1-z1z&(((K94xN}8a_#96+5eKdzf7P4eN$F6 zKguxbF^bu6FSYcqKREy&GntPhw31r-k0o;^V2&}=lCp*7XN14|uZR)IT<*%$a>m!V{X?D-PU=%Zz#a3M3#Pq@{ zzLyYvUzyi zbT0IsNoy>gJl~gl*~YQe&yw}jvqg^^lLM|6cs*A%GGG;g&)K2PEPLXneQzCVIzf`w zxtQ-^vK~f*ww5&C2L}W=q3|anqjK<3fC&B_6aU(Bq_81==4G`Un7iz!o48Hai2CM3 z#G62HkOX3gI1od)On0o9rGw;v_yGmt2R9Hu5=uWpEppCYvj?tm0N1ovsi#F=mgE(X z%~D=fn3a@Dt8EH<1^m>|&UDVplWDnYrAP=>@1sDC(-GibjZad@7(`pNXC}g`b7y+! z9yhuVL1o!MuJ)$&zfpn2H{TL;q7v0#bVW$Wx4`xv#

<>1D7Os%hsFNv8w*3W*oq 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 JO!U~X{{^W*G^PLm literal 0 HcmV?d00001 diff --git a/lib/src/modules/olecf/tests/testdata/cc354533e3a8190985784e476d6e16cc04f43f53935a885c99c21148c975a705.out b/lib/src/modules/olecf/tests/testdata/cc354533e3a8190985784e476d6e16cc04f43f53935a885c99c21148c975a705.out new file mode 100644 index 000000000..8867215a1 --- /dev/null +++ b/lib/src/modules/olecf/tests/testdata/cc354533e3a8190985784e476d6e16cc04f43f53935a885c99c21148c975a705.out @@ -0,0 +1,2 @@ +olecf: + is_olecf: true \ No newline at end of file diff --git a/lib/src/modules/olecf/tests/testdata/cc354533e3a8190985784e476d6e16cc04f43f53935a885c99c21148c975a705.zip b/lib/src/modules/olecf/tests/testdata/cc354533e3a8190985784e476d6e16cc04f43f53935a885c99c21148c975a705.zip new file mode 100644 index 0000000000000000000000000000000000000000..2e6f897dee44f3ac22bd78749fd0b3c6c2b13947 GIT binary patch literal 688 zcmWIWW@Zs#-~htvuBnj>P_TiMfx($Ufgw5B*wn<-*f`ZV(ZbNuz|z9h+`=T)#M~^! zEY;8~IoZG@&BQp()Y#J4G||GsG}+QJ*~rk)#3I?!+%(bLz*H|YFEoUgft|}fJspHY zex;_DR&X;gvVbfC6TMf@<{efLV0lozWY1|m34^_fb@%>H~!onO9KRo+{eVbAF!~@m^N(D^!IAwqiXD(1Q*cbLrVFUX*ma+rA2e|HV>Ktf3 zaQMTii|%)H7sYYjX^?Hy-k|=0agX2*hTzJ$in_SJyKPqMmFUVouKUdv;LXmVCclNV vf{}p%6x{*dj7%cTh@^ll2TBSsu&oiqqC>(6@MdKLS;Yv16M-}*6Nm=@KZN&< literal 0 HcmV?d00001 diff --git a/lib/src/modules/protos/mods.proto b/lib/src/modules/protos/mods.proto index 486d8d96d..94bdd4a9f 100644 --- a/lib/src/modules/protos/mods.proto +++ b/lib/src/modules/protos/mods.proto @@ -6,6 +6,8 @@ import "elf.proto"; import "pe.proto"; import "lnk.proto"; import "macho.proto"; +import "olecf.proto"; +import "vba.proto"; package mods; @@ -16,4 +18,6 @@ message Modules { optional dotnet.Dotnet dotnet = 3; optional macho.Macho macho = 4; optional lnk.Lnk lnk = 5; + optional olecf.Olecf olecf = 6; + optional vba.Vba vba = 7; } \ No newline at end of file diff --git a/lib/src/modules/protos/olecf.proto b/lib/src/modules/protos/olecf.proto new file mode 100644 index 000000000..6b42b3a3a --- /dev/null +++ b/lib/src/modules/protos/olecf.proto @@ -0,0 +1,22 @@ +syntax = "proto2"; +import "yara.proto"; + +package olecf; + +option (yara.module_options) = { + name : "olecf" + root_message: "olecf.Olecf" + rust_module: "olecf" + cargo_feature: "olecf-module" +}; + +message Olecf { + // Check if file is an OLE CF file + required bool is_olecf = 1; + + // Get array of stream names + repeated string stream_names = 2; + + // Get size of a specific stream by name + repeated int64 stream_sizes = 3; +} \ No newline at end of file diff --git a/lib/src/modules/protos/vba.proto b/lib/src/modules/protos/vba.proto new file mode 100644 index 000000000..e23a054c4 --- /dev/null +++ b/lib/src/modules/protos/vba.proto @@ -0,0 +1,37 @@ +syntax = "proto2"; +import "yara.proto"; + +package vba; + +option (yara.module_options) = { + name: "vba" + root_message: "vba.Vba" + rust_module: "vba" + cargo_feature: "vba-module" +}; + +message Vba { + // True if VBA macros are present + optional bool has_macros = 1; + + // Names of VBA macro modules found + repeated string module_names = 2; + + // Type of each module (standard, class, form) + repeated string module_types = 3; + + // The actual VBA code for each module + repeated string module_codes = 4; + + // Project metadata + message ProjectInfo { + optional string name = 1; + optional string version = 2; + repeated string references = 3; + + // Additional metadata + optional int32 module_count = 4; + optional bool is_compressed = 5; + } + optional ProjectInfo project_info = 5; +} \ No newline at end of file diff --git a/lib/src/modules/vba/mod.rs b/lib/src/modules/vba/mod.rs new file mode 100644 index 000000000..23daf4980 --- /dev/null +++ b/lib/src/modules/vba/mod.rs @@ -0,0 +1,207 @@ +/*! YARA module that extracts VBA (Visual Basic for Applications) macros from Office documents. + +Read more about the VBA file format specification here: + https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/575462ba-bf67-4190-9fac-c275523c75fc +*/ + +use crate::modules::prelude::*; +use crate::modules::protos::vba::*; +use crate::modules::protos::vba::vba::ProjectInfo; +use protobuf::MessageField; +use std::collections::HashMap; +use std::io::Read; +use std::io::Cursor; +use zip::ZipArchive; + +mod parser; +use parser::{VbaProject, ModuleType}; + +#[derive(Debug)] +struct VbaExtractor<'a> { + data: &'a [u8], + } + +impl<'a> VbaExtractor<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data } + } + + fn is_zip(&self) -> bool { + let result = self.data.starts_with(&[0x50, 0x4B, 0x03, 0x04]); + result + } + + fn read_stream(&self, ole_parser: &crate::modules::olecf::parser::OLECFParser, name: &str) -> Result, &'static str> { + let size = ole_parser.get_stream_size(name)? as usize; + + // Skip empty streams + if size == 0 { + return Err("Stream is empty"); + } + + let data = ole_parser.get_stream_data(name)?; + + Ok(data) + } + + fn extract_from_ole(&self) -> Result { + let ole_parser = crate::modules::olecf::parser::OLECFParser::new(&self.data)?; + let stream_names = ole_parser.get_stream_names()?; + + let mut vba_dir = None; + let mut modules = HashMap::new(); + let mut project_streams = Vec::new(); + + // First process the dir stream + if let Some(dir_name) = stream_names.iter().find(|n| n.to_lowercase().trim() == "dir") { + match self.read_stream(&ole_parser, dir_name) { + Ok(data) => { + vba_dir = Some(data); + }, + Err(_) => (), + } + } + + // Then process other streams + for name in &stream_names { + let lowercase_name = name.to_lowercase(); + + if lowercase_name != "dir" { + if lowercase_name.contains("module") || + lowercase_name.contains("thisdocument") || + lowercase_name.ends_with(".bas") || + lowercase_name.ends_with(".cls") || + lowercase_name.ends_with(".frm") { + if let Ok(data) = self.read_stream(&ole_parser, name) { + if !data.is_empty() { + modules.insert(name.clone(), data); + println!("Added module: {}", name); + } + } + } else if lowercase_name.contains("project") && !lowercase_name.contains("_vba_project") { + if let Ok(data) = self.read_stream(&ole_parser, name) { + project_streams.push(data); + } + } + } + } + + // Always try the dir stream first if we found it + if let Some(dir_data) = vba_dir { + parser::VbaProject::parse(&dir_data, modules) + } else { + Err("No VBA directory stream found") + } + } + + fn extract_from_zip(&self) -> Result { + let reader = Cursor::new(&self.data); + let mut archive = ZipArchive::new(reader) + .map_err(|_| "Failed to read ZIP archive")?; + + // Search for potential VBA project files + let vba_project_names = [ + "word/vbaProject.bin", + "xl/vbaProject.bin", + "ppt/vbaProject.bin", + "vbaProject.bin" + ]; + + for name in &vba_project_names { + match archive.by_name(name) { + Ok(mut file) => { + let mut contents = Vec::new(); + file.read_to_end(&mut contents) + .map_err(|_| "Failed to read vbaProject.bin")?; + + // Parse as OLE + let ole_parser = crate::modules::olecf::parser::OLECFParser::new(&contents)?; + let stream_names = ole_parser.get_stream_names()?; + + let mut vba_dir = None; + let mut modules = HashMap::new(); + + for stream_name in &stream_names { + let _stream_size = ole_parser.get_stream_size(stream_name)?; + + if stream_name.starts_with("dir") { + match self.read_stream(&ole_parser, stream_name) { + Ok(data) => { + if !data.is_empty() { + vba_dir = Some(data); + } + }, + Err(_) => (), + } + } + } + + // Process other streams + for name in &stream_names { + if let Ok(data) = self.read_stream(&ole_parser, name) { + if !data.is_empty() { + modules.insert(name.clone(), data); + } + } + } + + // Use dir stream if found, otherwise fail + if let Some(dir_data) = vba_dir { + return parser::VbaProject::parse(&dir_data, modules); + } + }, + Err(_) => continue, + } + } + + Err("No VBA project found in ZIP") + } +} + +#[module_main] +fn main(data: &[u8], _meta: Option<&[u8]>) -> Vba { + let mut vba = Vba::new(); + vba.has_macros = Some(false); + + let extractor = VbaExtractor::new(data); + + let project_result = if extractor.is_zip() { + extractor.extract_from_zip() + } else { + extractor.extract_from_ole() + }; + + match project_result { + Ok(project) => { + vba.has_macros = Some(true); + + let mut project_info = ProjectInfo::new(); + project_info.name = Some(project.info.name.clone()); + project_info.version = Some(project.info.version.clone()); + project_info.references = project.info.references.clone(); + + // Add metadata + let module_count = project.modules.len() as i32; + project_info.module_count = Some(module_count); + project_info.is_compressed = Some(true); + + vba.project_info = MessageField::some(project_info); + + // Process modules + for module in project.modules.values() { + vba.module_names.push(module.name.clone()); + vba.module_types.push(match module.module_type { + ModuleType::Standard => "Standard".to_string(), + ModuleType::Class => "Class".to_string(), + ModuleType::Unknown => "Unknown".to_string(), + }); + vba.module_codes.push(module.code.clone()); + } + }, + Err(_) => { + vba.has_macros = Some(false); + } + } + + vba +} \ No newline at end of file diff --git a/lib/src/modules/vba/parser.rs b/lib/src/modules/vba/parser.rs new file mode 100644 index 000000000..f65c48bba --- /dev/null +++ b/lib/src/modules/vba/parser.rs @@ -0,0 +1,662 @@ +use std::collections::HashMap; +use nom::{ + number::complete::{le_u16, le_u32}, +}; + +pub enum ModuleType { + Standard, + Class, + Unknown, +} + +pub struct ProjectInfo { + pub name: String, + pub version: String, + pub references: Vec, +} + +pub struct VbaModule { + pub name: String, + pub code: String, + pub module_type: ModuleType, +} + +pub struct VbaProject { + pub modules: HashMap, + pub info: ProjectInfo, +} + +impl VbaProject { + fn copytoken_help(difference: usize) -> (u16, u16, u32, u16) { + let bit_count = (difference as f64).log2().ceil() as u32; + let bit_count = bit_count.max(4); + let length_mask = 0xFFFF >> bit_count; + let offset_mask = !length_mask; + let maximum_length = (0xFFFF >> bit_count) + 3; + + (length_mask, offset_mask, bit_count, maximum_length) + } + + pub fn decompress_stream(compressed: &[u8]) -> Result, &'static str> { + if compressed.is_empty() { + return Err("Empty input buffer"); + } + + if compressed[0] != 0x01 { + return Err("Invalid signature byte"); + } + + let mut decompressed = Vec::new(); + let mut current = 1; // Skip signature byte + + while current < compressed.len() { + // We need 2 bytes for the chunk header + if current + 2 > compressed.len() { + return Err("Incomplete chunk header"); + } + + let chunk_header = u16::from_le_bytes( + compressed[current..current+2].try_into().map_err(|_| "Failed to parse chunk header")? + ); + let chunk_size = (chunk_header & 0x0FFF) as usize + 3; + let chunk_is_compressed = (chunk_header & 0x8000) != 0; + + current += 2; + + if chunk_is_compressed && chunk_size > 4095 { + return Err("CompressedChunkSize > 4095 but CompressedChunkFlag == 1"); + } + if !chunk_is_compressed && chunk_size != 4095 { + return Err("CompressedChunkSize != 4095 but CompressedChunkFlag == 0"); + } + + let chunk_end = std::cmp::min(compressed.len(), current + chunk_size); + + if !chunk_is_compressed { + if current + 4096 > compressed.len() { + return Err("Incomplete uncompressed chunk"); + } + decompressed.extend_from_slice(&compressed[current..current + 4096]); + current += 4096; + continue; + } + + let decompressed_chunk_start = decompressed.len(); + + while current < chunk_end { + let flag_byte = compressed[current]; + current += 1; + + for bit_index in 0..8 { + if current >= chunk_end { + break; + } + + if (flag_byte & (1 << bit_index)) == 0 { + decompressed.push(compressed[current]); + current += 1; + } else { + if current + 2 > compressed.len() { + return Err("Incomplete copy token"); + } + + let copy_token = u16::from_le_bytes( + compressed[current..current+2].try_into().map_err(|_| "Failed to parse copy token")? + ); + let (length_mask, offset_mask, bit_count, _) = + Self::copytoken_help(decompressed.len() - decompressed_chunk_start); + + let length = (copy_token & length_mask) + 3; + let temp1 = copy_token & offset_mask; + let temp2 = 16 - bit_count; + let offset = u16::try_from((temp1 >> temp2) + 1) + .map_err(|_| "Offset calculation overflow")?; + + if offset as usize > decompressed.len() { + return Err("Invalid copy token offset"); + } + + let copy_source = decompressed.len() - offset as usize; + for i in 0..length { + let source_idx = copy_source + i as usize; + if source_idx >= decompressed.len() { + return Err("Copy token source out of bounds"); + } + decompressed.push(decompressed[source_idx]); + } + current += 2; + } + } + } + } + + Ok(decompressed) + } + + fn parse_u16(input: &[u8]) -> Result<(&[u8], u16), &'static str> { + le_u16::<&[u8], nom::error::Error<&[u8]>>(input) + .map_err(|_nom_err| "Failed to parse u16") + } + + fn parse_u32(input: &[u8]) -> Result<(&[u8], u32), &'static str> { + le_u32::<&[u8], nom::error::Error<&[u8]>>(input) + .map_err(|_nom_err| "Failed to parse u32") + } + + fn parse_bytes<'a>(input: &'a [u8], len: usize) -> Result<(&'a [u8], &'a [u8]), &'static str> { + if input.len() < len { + Err("Not enough bytes to parse the requested slice") + } else { + Ok((&input[len..], &input[..len])) + } + } + + pub fn parse(compressed_dir_stream: &[u8], module_streams: HashMap>) -> Result { + let dir_stream = Self::decompress_stream(compressed_dir_stream)?; + + // Our 'input' will move forward as we parse + let mut _input = &dir_stream[..]; + + // -- PROJECTSYSKIND Record + let (rest, syskind_id) = Self::parse_u16(_input)?; _input = rest; + if syskind_id != 0x0001 { + return Err("Invalid SYSKIND_ID"); + } + let (rest, syskind_size) = Self::parse_u32(_input)?; _input = rest; + if syskind_size != 0x0004 { + return Err("Invalid SYSKIND_SIZE"); + } + let (rest, _syskind) = Self::parse_u32(_input)?; _input = rest; + + // -- PROJECTLCID Record + let (rest, lcid_id) = Self::parse_u16(_input)?; _input = rest; + if lcid_id != 0x0002 { + return Err("Invalid LCID_ID"); + } + let (rest, lcid_size) = Self::parse_u32(_input)?; _input = rest; + if lcid_size != 0x0004 { + return Err("Invalid LCID_SIZE"); + } + let (rest, lcid) = Self::parse_u32(_input)?; _input = rest; + if lcid != 0x409 { + return Err("Invalid LCID"); + } + + // -- PROJECTLCIDINVOKE Record + let (rest, lcid_invoke_id) = Self::parse_u16(_input)?; _input = rest; + if lcid_invoke_id != 0x0014 { + return Err("Invalid LCIDINVOKE_ID"); + } + let (rest, lcid_invoke_size) = Self::parse_u32(_input)?; _input = rest; + if lcid_invoke_size != 0x0004 { + return Err("Invalid LCIDINVOKE_SIZE"); + } + let (rest, lcid_invoke) = Self::parse_u32(_input)?; _input = rest; + if lcid_invoke != 0x409 { + return Err("Invalid LCIDINVOKE"); + } + + // -- PROJECTCODEPAGE Record + let (rest, codepage_id) = Self::parse_u16(_input)?; _input = rest; + if codepage_id != 0x0003 { + return Err("Invalid CODEPAGE_ID"); + } + let (rest, codepage_size) = Self::parse_u32(_input)?; _input = rest; + if codepage_size != 0x0002 { + return Err("Invalid CODEPAGE_SIZE"); + } + let (rest, _codepage) = Self::parse_u16(_input)?; _input = rest; + + // -- PROJECTNAME Record + let (rest, name_id) = Self::parse_u16(_input)?; _input = rest; + if name_id != 0x0004 { + return Err("Invalid NAME_ID"); + } + let (rest, name_size) = Self::parse_u32(_input)?; _input = rest; + let name_size = name_size as usize; + if name_size < 1 || name_size > 128 { + return Err("Project name not in valid range"); + } + let (rest, name_bytes) = Self::parse_bytes(rest, name_size)?; + let project_name = String::from_utf8_lossy(name_bytes).to_string(); + _input = rest; + + // -- PROJECTDOCSTRING Record + let (rest, doc_id) = Self::parse_u16(_input)?; _input = rest; + if doc_id != 0x0005 { + return Err("Invalid DOCSTRING_ID"); + } + let (rest, doc_size) = Self::parse_u32(_input)?; _input = rest; + let doc_size = doc_size as usize; + let (rest, _doc_string) = Self::parse_bytes(rest, doc_size)?; + _input = rest; + let (rest, doc_reserved) = Self::parse_u16(_input)?; _input = rest; + if doc_reserved != 0x0040 { + return Err("Invalid DOCSTRING_Reserved"); + } + let (rest, doc_unicode_size) = Self::parse_u32(_input)?; _input = rest; + let doc_unicode_size = doc_unicode_size as usize; + if doc_unicode_size % 2 != 0 { + return Err("DOCSTRING_Unicode size not even"); + } + let (rest, _doc_unicode) = Self::parse_bytes(rest, doc_unicode_size)?; + _input = rest; + + // -- PROJECTHELPFILEPATH Record + let (rest, helpfile_id) = Self::parse_u16(_input)?; _input = rest; + if helpfile_id != 0x0006 { + return Err("Invalid HELPFILEPATH_ID"); + } + let (rest, helpfile_size1) = Self::parse_u32(_input)?; _input = rest; + let helpfile_size1 = helpfile_size1 as usize; + if helpfile_size1 > 260 { + return Err("Help file path 1 too long"); + } + let (rest, helpfile1) = Self::parse_bytes(rest, helpfile_size1)?; + _input = rest; + let (rest, helpfile_reserved) = Self::parse_u16(_input)?; _input = rest; + if helpfile_reserved != 0x003D { + return Err("Invalid HELPFILEPATH_Reserved"); + } + let (rest, helpfile_size2) = Self::parse_u32(_input)?; _input = rest; + let helpfile_size2 = helpfile_size2 as usize; + if helpfile_size2 != helpfile_size1 { + return Err("Help file sizes don't match"); + } + let (rest, helpfile2) = Self::parse_bytes(rest, helpfile_size2)?; + _input = rest; + if helpfile1 != helpfile2 { + return Err("Help files don't match"); + } + + // -- PROJECTHELPCONTEXT Record + let (rest, helpcontext_id) = Self::parse_u16(_input)?; _input = rest; + if helpcontext_id != 0x0007 { + return Err("Invalid HELPCONTEXT_ID"); + } + let (rest, helpcontext_size) = Self::parse_u32(_input)?; _input = rest; + if helpcontext_size != 0x0004 { + return Err("Invalid HELPCONTEXT_SIZE"); + } + let (rest, _helpcontext) = Self::parse_u32(_input)?; _input = rest; + + // -- PROJECTLIBFLAGS Record + let (rest, libflags_id) = Self::parse_u16(_input)?; _input = rest; + if libflags_id != 0x0008 { + return Err("Invalid LIBFLAGS_ID"); + } + let (rest, libflags_size) = Self::parse_u32(_input)?; _input = rest; + if libflags_size != 0x0004 { + return Err("Invalid LIBFLAGS_SIZE"); + } + let (rest, libflags) = Self::parse_u32(_input)?; _input = rest; + if libflags != 0x0000 { + return Err("Invalid LIBFLAGS"); + } + + // -- PROJECTVERSION Record + let (rest, version_id) = Self::parse_u16(_input)?; _input = rest; + if version_id != 0x0009 { + return Err("Invalid VERSION_ID"); + } + let (rest, version_reserved) = Self::parse_u32(_input)?; _input = rest; + if version_reserved != 0x0004 { + return Err("Invalid VERSION_Reserved"); + } + let (rest, version_major) = Self::parse_u32(_input)?; _input = rest; + let (rest, version_minor) = Self::parse_u16(_input)?; _input = rest; + + // -- PROJECTCONSTANTS Record + let (rest, constants_id) = Self::parse_u16(_input)?; _input = rest; + if constants_id != 0x000C { + return Err("Invalid CONSTANTS_ID"); + } + let (rest, constants_size) = Self::parse_u32(_input)?; _input = rest; + let constants_size = constants_size as usize; + if constants_size > 1015 { + return Err("Constants size too large"); + } + let (rest, _constants) = Self::parse_bytes(rest, constants_size)?; + _input = rest; + let (rest, constants_reserved) = Self::parse_u16(_input)?; _input = rest; + if constants_reserved != 0x003C { + return Err("Invalid CONSTANTS_Reserved"); + } + let (rest, constants_unicode_size) = Self::parse_u32(_input)?; _input = rest; + let constants_unicode_size = constants_unicode_size as usize; + if constants_unicode_size % 2 != 0 { + return Err("Constants unicode size not even"); + } + let (rest, _constants_unicode) = Self::parse_bytes(rest, constants_unicode_size)?; + _input = rest; + + // -- Parse references until we hit PROJECTMODULES_Id = 0x000F + let mut references = Vec::new(); + let mut last_check; + loop { + let (rest2, check) = match Self::parse_u16(_input) { + Ok(x) => x, + Err(_) => return Err("Could not parse reference type (u16)"), + }; + _input = rest2; + last_check = check; + + if check == 0x000F { + // That means we reached PROJECTMODULES_Id + break; + } + + match check { + 0x0016 => { + // REFERENCE Name + let (rest2, name_size) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, name_bytes) = Self::parse_bytes(_input, name_size as usize)?; + _input = rest2; + let name = String::from_utf8_lossy(name_bytes).to_string(); + references.push(name); + + let (rest2, reserved) = Self::parse_u16(_input)?; _input = rest2; + if reserved != 0x003E { + return Err("Invalid REFERENCE_Reserved"); + } + let (rest2, unicode_size) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, _name_unicode) = Self::parse_bytes(_input, unicode_size as usize)?; + _input = rest2; + }, + 0x0033 => { + // REFERENCEORIGINAL + let (rest2, size) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, _libid) = Self::parse_bytes(_input, size as usize)?; + _input = rest2; + }, + 0x002F => { + // REFERENCECONTROL + let (rest2, size_twiddled) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, _twiddled) = Self::parse_bytes(_input, size_twiddled as usize)?; + _input = rest2; + + let (rest2, reserved1) = Self::parse_u32(_input)?; _input = rest2; + if reserved1 != 0x0000 { + return Err("Invalid REFERENCECONTROL_Reserved1"); + } + let (rest2, reserved2) = Self::parse_u16(_input)?; _input = rest2; + if reserved2 != 0x0000 { + return Err("Invalid REFERENCECONTROL_Reserved2"); + } + + // Possibly an optional name record + let (maybe_rest, maybe_check2) = match Self::parse_u16(_input) { + Ok(x) => x, + Err(_) => return Err("Failed to read optional name or reserved3"), + }; + + if maybe_check2 == 0x0016 { + // This means we have a name record + _input = maybe_rest; + let (rest2, name_size) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, _name) = Self::parse_bytes(_input, name_size as usize)?; + _input = rest2; + + let (rest2, reserved) = Self::parse_u16(_input)?; _input = rest2; + if reserved != 0x003E { + return Err("Invalid REFERENCECONTROL_NameRecord_Reserved"); + } + let (rest2, unicode_size) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, _name_unicode) = Self::parse_bytes(_input, unicode_size as usize)?; + _input = rest2; + + // Next we parse the next 0x0030 + let (rest2, reserved3) = Self::parse_u16(_input)?; _input = rest2; + if reserved3 != 0x0030 { + return Err("Invalid REFERENCECONTROL_Reserved3"); + } + } else { + // No name record, so maybe_check2 is actually reserved3 + _input = maybe_rest; + if maybe_check2 != 0x0030 { + return Err("Invalid REFERENCECONTROL_Reserved3"); + } + } + + let (rest2, size_extended) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, size_libid) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, _libid) = Self::parse_bytes(_input, size_libid as usize)?; + _input = rest2; + let (rest2, _reserved4) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, _reserved5) = Self::parse_u16(_input)?; _input = rest2; + let (rest2, _original_typelib) = Self::parse_bytes(_input, 16)?; + _input = rest2; + let (rest2, _cookie) = Self::parse_u32(_input)?; _input = rest2; + let _ = size_extended; // just to avoid unused var warnings + }, + 0x000D => { + // REFERENCEREGISTERED + let (rest2, _size) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, libid_size) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, _libid) = Self::parse_bytes(_input, libid_size as usize)?; + _input = rest2; + let (rest2, reserved1) = Self::parse_u32(_input)?; _input = rest2; + if reserved1 != 0x0000 { + return Err("Invalid REFERENCEREGISTERED_Reserved1"); + } + let (rest2, reserved2) = Self::parse_u16(_input)?; _input = rest2; + if reserved2 != 0x0000 { + return Err("Invalid REFERENCEREGISTERED_Reserved2"); + } + }, + 0x000E => { + // REFERENCEPROJECT + let (rest2, _size) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, libid_abs_size) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, _libid_abs) = Self::parse_bytes(_input, libid_abs_size as usize)?; + _input = rest2; + let (rest2, libid_rel_size) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, _libid_rel) = Self::parse_bytes(_input, libid_rel_size as usize)?; + _input = rest2; + let (rest2, _major) = Self::parse_u32(_input)?; _input = rest2; + let (rest2, _minor) = Self::parse_u16(_input)?; _input = rest2; + }, + _ => return Err("Invalid reference type"), + } + } + + if last_check != 0x000F { + return Err("Invalid PROJECTMODULES_Id"); + } + + let (rest, modules_size) = Self::parse_u32(_input)?; _input = rest; + if modules_size != 0x0002 { + return Err("Invalid PROJECTMODULES_Size"); + } + + let (rest, modules_count) = Self::parse_u16(_input)?; _input = rest; + + let (rest, cookie_id) = Self::parse_u16(_input)?; _input = rest; + if cookie_id != 0x0013 { + return Err("Invalid ProjectCookie_Id"); + } + + let (rest, cookie_size) = Self::parse_u32(_input)?; _input = rest; + if cookie_size != 0x0002 { + return Err("Invalid ProjectCookie_Size"); + } + + let (rest, _cookie) = Self::parse_u16(_input)?; + _input = rest; + + // -- Parse each module + let mut modules = HashMap::new(); + for _ in 0..modules_count { + // MODULENAME record + let (rest2, module_id) = Self::parse_u16(_input)?; + _input = rest2; + if module_id != 0x0019 { + return Err("Invalid MODULENAME_Id"); + } + + let (rest2, module_name_size) = Self::parse_u32(_input)?; + _input = rest2; + let (rest2, name_bytes) = Self::parse_bytes(_input, module_name_size as usize)?; + _input = rest2; + let module_name = String::from_utf8_lossy(name_bytes).to_string(); + + let mut module_type = ModuleType::Unknown; + let mut stream_name = String::new(); + let mut module_offset = 0u32; + + // Read all sections until we get the terminator 0x002B + loop { + let (rest2, section_id) = match Self::parse_u16(_input) { + Ok(x) => x, + Err(_) => return Err("Failed to parse module section ID"), + }; + _input = rest2; + + match section_id { + 0x0047 => { + // MODULENAMEUNICODE + let (rest3, unicode_size) = Self::parse_u32(_input)?; + _input = rest3; + let (rest3, _unicode_name) = Self::parse_bytes(_input, unicode_size as usize)?; + _input = rest3; + }, + 0x001A => { + // MODULESTREAMNAME + let (rest3, stream_size) = Self::parse_u32(_input)?; + _input = rest3; + let (rest3, stream_bytes) = Self::parse_bytes(_input, stream_size as usize)?; + _input = rest3; + stream_name = String::from_utf8_lossy(stream_bytes).to_string(); + + let (rest3, reserved) = Self::parse_u16(_input)?; + _input = rest3; + if reserved != 0x0032 { + return Err("Invalid STREAMNAME_Reserved"); + } + + let (rest3, unicode_size) = Self::parse_u32(_input)?; + _input = rest3; + let (rest3, _unicode_name) = Self::parse_bytes(_input, unicode_size as usize)?; + _input = rest3; + }, + 0x001C => { + // MODULEDOCSTRING + let (rest3, doc_size) = Self::parse_u32(_input)?; + _input = rest3; + let (rest3, _doc_string) = Self::parse_bytes(_input, doc_size as usize)?; + _input = rest3; + + let (rest3, reserved) = Self::parse_u16(_input)?; + _input = rest3; + if reserved != 0x0048 { + return Err("Invalid DOCSTRING_Reserved"); + } + + let (rest3, unicode_size) = Self::parse_u32(_input)?; + _input = rest3; + let (rest3, _unicode_doc) = Self::parse_bytes(_input, unicode_size as usize)?; + _input = rest3; + }, + 0x0031 => { + // MODULEOFFSET + let (rest3, offset_size) = Self::parse_u32(_input)?; + _input = rest3; + if offset_size != 0x0004 { + return Err("Invalid OFFSET_Size"); + } + let (rest3, offset) = Self::parse_u32(_input)?; + module_offset = offset; + _input = rest3; + }, + 0x001E => { + // MODULEHELPCONTEXT + let (rest3, help_size) = Self::parse_u32(_input)?; + _input = rest3; + if help_size != 0x0004 { + return Err("Invalid HELPCONTEXT_Size"); + } + let (rest3, _help_context) = Self::parse_u32(_input)?; + _input = rest3; + }, + 0x002C => { + // MODULECOOKIE + let (rest3, cookie_size) = Self::parse_u32(_input)?; + _input = rest3; + if cookie_size != 0x0002 { + return Err("Invalid COOKIE_Size"); + } + let (rest3, _cookie) = Self::parse_u16(_input)?; + _input = rest3; + }, + 0x0021 => { + // Module is Standard + module_type = ModuleType::Standard; + let (rest3, _reserved) = Self::parse_u32(_input)?; + _input = rest3; + }, + 0x0022 => { + // Module is Class + module_type = ModuleType::Class; + let (rest3, _reserved) = Self::parse_u32(_input)?; + _input = rest3; + }, + 0x0025 => { + // MODULEREADONLY + let (rest3, reserved) = Self::parse_u32(_input)?; + _input = rest3; + if reserved != 0x0000 { + return Err("Invalid READONLY_Reserved"); + } + }, + 0x0028 => { + // MODULEPRIVATE + let (rest3, reserved) = Self::parse_u32(_input)?; + _input = rest3; + if reserved != 0x0000 { + return Err("Invalid PRIVATE_Reserved"); + } + }, + 0x002B => { + // TERMINATOR + let (rest3, reserved) = Self::parse_u32(_input)?; + if reserved != 0x0000 { + return Err("Invalid MODULE_Reserved"); + } + _input = rest3; + break; + }, + _ => return Err("Invalid module section ID"), + } + } + + // Retrieve module code + if let Some(module_data) = module_streams.get(&stream_name) { + if module_offset as usize >= module_data.len() { + return Err("Invalid module offset"); + } + let code_data = &module_data[module_offset as usize..]; + if !code_data.is_empty() { + let decompressed = Self::decompress_stream(code_data)?; + let code = String::from_utf8_lossy(&decompressed).to_string(); + modules.insert( + module_name.clone(), + VbaModule { + name: module_name, + code, + module_type, + } + ); + } + } + } + + Ok(VbaProject { + modules, + info: ProjectInfo { + name: project_name, + version: format!("{}.{}", version_major, version_minor), + references, + }, + }) + } +} \ No newline at end of file diff --git a/lib/src/modules/vba/tests/testdata/643d1e3b68c1e31aef5779eb28ac3b0aaa284c91c47c26cfc2dbb3bc7f569103.out b/lib/src/modules/vba/tests/testdata/643d1e3b68c1e31aef5779eb28ac3b0aaa284c91c47c26cfc2dbb3bc7f569103.out new file mode 100644 index 000000000..408dca385 --- /dev/null +++ b/lib/src/modules/vba/tests/testdata/643d1e3b68c1e31aef5779eb28ac3b0aaa284c91c47c26cfc2dbb3bc7f569103.out @@ -0,0 +1,17 @@ +vba: + has_macros: true + module_names: + - "ThisDocument" + module_types: + - "Class" + module_code: + - "Attribute VB_Name = \"ThisDocument\"\r\nAttribute VB_Base = \"1Normal.ThisDocument\"\r\nAttribute VB_GlobalNameSpace = False\r\nAttribute VB_Creatable = False\r\nAttribute VB_PredeclaredId = True\r\nAttribute VB_Exposed = True\r\nAttribute VB_TemplateDerived = True\r\nAttribute VB_Customizable = True\r\n\r\nPrivate Sub Document_New()\r\n MsgBox \"Hello, world!\"\r\nEnd Sub\r\n" + project_info: + name: "Project" + version: "1769106437.10" + references: + - "stdole" + - "Normal" + - "Office" + module_count: 1 + is_compressed: true \ No newline at end of file diff --git a/lib/src/modules/vba/tests/testdata/643d1e3b68c1e31aef5779eb28ac3b0aaa284c91c47c26cfc2dbb3bc7f569103.zip b/lib/src/modules/vba/tests/testdata/643d1e3b68c1e31aef5779eb28ac3b0aaa284c91c47c26cfc2dbb3bc7f569103.zip new file mode 100644 index 0000000000000000000000000000000000000000..d12d5ee9860e3b7c2ff194257da4ac33ab05cf79 GIT binary patch literal 22055 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 Su0O9j0_@L#<3r~^SN{S3!2q}b literal 0 HcmV?d00001 diff --git a/lib/src/modules/vba/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.out b/lib/src/modules/vba/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.out new file mode 100644 index 000000000..a1340cbc3 --- /dev/null +++ b/lib/src/modules/vba/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.out @@ -0,0 +1,2 @@ +vba: + has_macros: false \ No newline at end of file diff --git a/lib/src/modules/vba/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.zip b/lib/src/modules/vba/tests/testdata/8de0e0bba84e2f80c2e2b58b66224f0d3a780f44fbb04fcf7caae34b973eb766.zip new file mode 100644 index 0000000000000000000000000000000000000000..2236f51f39bfdcd1705e11d9cd47f61a3199e85a GIT binary patch literal 12956 zcmdUWcUTkK+OLQRNS7{6bR#Ie2LeP;K}Em{2uKwoAWb@i1TcUoMNlbHB2ra)?=1qG z8jAE1AVNf1AV7$OVP>sav)=x$_5S9zA^i!SV@$vY{PNwy zV}JdVJv?^o%CWP@K#v_292M;C?LaDyN=_gJ2PH=(dsUFVnwpZ5ij%@)Wjl3{f|H7h zlfAuyij#wrx`Umaqq2&fc{`p(`oby-r zdFHPNFnM|ge72{MpMjVNH$bNdx*%qPF-ZEfMB$G>;Hcv^-D%`B-I-yM4k||SYV)_h z%v83!hKVlyh`(+=@@)6&hB!c*+=7v^5(1*{={2X2O+^;NgXOTAy6!{bz zz#Hq)RX+t;T8la){_5uuF^)$x-j=v$ffcSk)#>fGZ7^l`DXxC|_xB7nn6kzs9~e1o z-t*8vw%7?hdD@*8y6=KaMqGwHXg^F&tVu?pyXF16R~es(O?kEa+J-rb+2NKw8sYn7 ze+>I3qSeV9bnC0UQLwa^X>9Vr4o5u2dV3Y?>>mJwrA#CR7g(b#eQV;{w<&~*?;*Y^ zm)ISr(hPc&VfO4M5&_`+ABdKa{Vjhl#CZw20i<$E6H?u%iE8AK*~1@z!xlijwj`-z z)$mq%1ZuA6F8BO%!nszk-Os?4k^7M9Ai+$Py#Dkv?i(SwyV*JZ5mxdRIhERQMAOY` zIre)Plo81kVTw!3v}tKPt<&s*NY0Gt_zpjv{@lEvP^2r2G&^` zo9<`V+4Vu}pWFO|m42u0A3T_WEj&Y85cMlg%o+C_eG&4!N<)0!^a(ysXQBd&?K@K@!{8eleX=7nd9B<}zh zP|T8ab4*Y=Yi2|0NxK_BH*M}DpaG!upHILr)y^=_@2#|L3TYKFP1}ere=zLr`>#9y zn_<|c@lgUZ|6=fe)Bi6MT2uG}Q}rh}>^RiNr7L6eGb<406!XK0Vh2g#l4m6x4Ps^x z_wj^R#}B>{H^fBYT39ntLG*@qQ?S@1-z1z&(((K94xN}8a_#96+5eKdzf7P4eN$F6 zKguxbF^bu6FSYcqKREy&GntPhw31r-k0o;^V2&}=lCp*7XN14|uZR)IT<*%$a>m!V{X?D-PU=%Zz#a3M3#Pq@{ zzLyYvUzyi zbT0IsNoy>gJl~gl*~YQe&yw}jvqg^^lLM|6cs*A%GGG;g&)K2PEPLXneQzCVIzf`w zxtQ-^vK~f*ww5&C2L}W=q3|anqjK<3fC&B_6aU(Bq_81==4G`Un7iz!o48Hai2CM3 z#G62HkOX3gI1od)On0o9rGw;v_yGmt2R9Hu5=uWpEppCYvj?tm0N1ovsi#F=mgE(X z%~D=fn3a@Dt8EH<1^m>|&UDVplWDnYrAP=>@1sDC(-GibjZad@7(`pNXC}g`b7y+! z9yhuVL1o!MuJ)$&zfpn2H{TL;q7v0#bVW$Wx4`xv#

<>1D7Os%hsFNv8w*3W*oq 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 JO!U~X{{^W*G^PLm literal 0 HcmV?d00001 diff --git a/lib/src/modules/vba/tests/testdata/c62c12501055319db152f092e263f65da037c4a6f7ec0112832b95916ac8a1fb.out b/lib/src/modules/vba/tests/testdata/c62c12501055319db152f092e263f65da037c4a6f7ec0112832b95916ac8a1fb.out new file mode 100644 index 000000000..408dca385 --- /dev/null +++ b/lib/src/modules/vba/tests/testdata/c62c12501055319db152f092e263f65da037c4a6f7ec0112832b95916ac8a1fb.out @@ -0,0 +1,17 @@ +vba: + has_macros: true + module_names: + - "ThisDocument" + module_types: + - "Class" + module_code: + - "Attribute VB_Name = \"ThisDocument\"\r\nAttribute VB_Base = \"1Normal.ThisDocument\"\r\nAttribute VB_GlobalNameSpace = False\r\nAttribute VB_Creatable = False\r\nAttribute VB_PredeclaredId = True\r\nAttribute VB_Exposed = True\r\nAttribute VB_TemplateDerived = True\r\nAttribute VB_Customizable = True\r\n\r\nPrivate Sub Document_New()\r\n MsgBox \"Hello, world!\"\r\nEnd Sub\r\n" + project_info: + name: "Project" + version: "1769106437.10" + references: + - "stdole" + - "Normal" + - "Office" + module_count: 1 + is_compressed: true \ No newline at end of file diff --git a/lib/src/modules/vba/tests/testdata/c62c12501055319db152f092e263f65da037c4a6f7ec0112832b95916ac8a1fb.zip b/lib/src/modules/vba/tests/testdata/c62c12501055319db152f092e263f65da037c4a6f7ec0112832b95916ac8a1fb.zip new file mode 100644 index 0000000000000000000000000000000000000000..0d14683301ae85cc19dca40f20d491fb8a5e0644 GIT binary patch literal 17631 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 literal 0 HcmV?d00001 From 00bca34cda50cef5a002346c417e6b51b6357d9e Mon Sep 17 00:00:00 2001 From: David Magnotti <78613347+davidmagnotti@users.noreply.github.com> Date: Sun, 12 Jan 2025 19:53:44 -0500 Subject: [PATCH 02/12] Fix for OOM from Infinite Loop - Addressed infinite loop issue in OLE CF parser. --- lib/src/modules/olecf/parser.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/src/modules/olecf/parser.rs b/lib/src/modules/olecf/parser.rs index 56a7ca705..6c7bcd462 100644 --- a/lib/src/modules/olecf/parser.rs +++ b/lib/src/modules/olecf/parser.rs @@ -247,14 +247,24 @@ impl<'a> OLECFParser<'a> { let mut current = start_sector; while current < MAX_REGULAR_SECTOR { + // Prevent cycles by keeping track of visited sectors + if chain.contains(¤t) { + // We've seen this sector before - it's a cycle + break; + } + chain.push(current); + let next = match self.get_fat_entry(current) { Ok(n) => n, Err(_) => break, }; + + // Check validity of next sector if next >= MAX_REGULAR_SECTOR || next == FREESECT || next == ENDOFCHAIN { break; } + current = next; } chain From 105269c1c7153854cd7ce98570ca35029738fe54 Mon Sep 17 00:00:00 2001 From: "Victor M. Alvarez" Date: Tue, 14 Jan 2025 12:50:37 +0100 Subject: [PATCH 03/12] fuzz: implement fuzzer for `vba` module. --- lib/fuzz/Cargo.toml | 6 ++++++ lib/fuzz/fuzz_targets/vba_parser.rs | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 lib/fuzz/fuzz_targets/vba_parser.rs diff --git a/lib/fuzz/Cargo.toml b/lib/fuzz/Cargo.toml index 0bd8d4fe6..79360e92c 100644 --- a/lib/fuzz/Cargo.toml +++ b/lib/fuzz/Cargo.toml @@ -47,6 +47,12 @@ path = "fuzz_targets/dotnet_parser.rs" test = false doc = false +[[bin]] +name = "vba_parser" +path = "fuzz_targets/vba_parser.rs" +test = false +doc = false + [[bin]] name = "rule_compiler" path = "fuzz_targets/rule_compiler.rs" diff --git a/lib/fuzz/fuzz_targets/vba_parser.rs b/lib/fuzz/fuzz_targets/vba_parser.rs new file mode 100644 index 000000000..343258f3f --- /dev/null +++ b/lib/fuzz/fuzz_targets/vba_parser.rs @@ -0,0 +1,6 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + let _ = yara_x::mods::invoke::(data); +}); From b26382722d735816bc360dc63977249be9f03f43 Mon Sep 17 00:00:00 2001 From: "Victor M. Alvarez" Date: Tue, 14 Jan 2025 13:23:19 +0100 Subject: [PATCH 04/12] style: fix clippy warnings. --- cli/src/commands/dump.rs | 2 +- lib/src/modules/olecf/parser.rs | 25 +++++++++++-------------- lib/src/modules/vba/mod.rs | 25 +++++++++---------------- lib/src/modules/vba/parser.rs | 7 +++---- 4 files changed, 24 insertions(+), 35 deletions(-) diff --git a/cli/src/commands/dump.rs b/cli/src/commands/dump.rs index cd0794ef3..9c9387c96 100644 --- a/cli/src/commands/dump.rs +++ b/cli/src/commands/dump.rs @@ -21,7 +21,7 @@ enum SupportedModules { Pe, Dotnet, Olecf, - Vba + Vba, } #[derive(Debug, Clone, ValueEnum)] diff --git a/lib/src/modules/olecf/parser.rs b/lib/src/modules/olecf/parser.rs index 56a7ca705..7bb1a6bda 100644 --- a/lib/src/modules/olecf/parser.rs +++ b/lib/src/modules/olecf/parser.rs @@ -163,20 +163,17 @@ impl<'a> OLECFParser<'a> { if abs_offset + DIRECTORY_ENTRY_SIZE as usize > self.data.len() { break; } - match self.read_directory_entry(abs_offset) { - Ok(entry) => { - if entry.stream_type == ROOT_STORAGE_TYPE { - self.mini_stream_start = entry.start_sector; - self.mini_stream_size = entry.size; - } - if entry.stream_type == STORAGE_TYPE - || entry.stream_type == STREAM_TYPE - || entry.stream_type == ROOT_STORAGE_TYPE - { - self.dir_entries.insert(entry.name.clone(), entry); - } + if let Ok(entry) = self.read_directory_entry(abs_offset) { + if entry.stream_type == ROOT_STORAGE_TYPE { + self.mini_stream_start = entry.start_sector; + self.mini_stream_size = entry.size; + } + if entry.stream_type == STORAGE_TYPE + || entry.stream_type == STREAM_TYPE + || entry.stream_type == ROOT_STORAGE_TYPE + { + self.dir_entries.insert(entry.name.clone(), entry); } - Err(_) => {} } entry_offset += DIRECTORY_ENTRY_SIZE as usize; } @@ -266,7 +263,7 @@ impl<'a> OLECFParser<'a> { } let name_len = parse_u16_at(self.data, offset + 64)? as usize; - if name_len < 2 || name_len > 64 { + if !(2..=64).contains(&name_len) { return Err("Invalid name length"); } diff --git a/lib/src/modules/vba/mod.rs b/lib/src/modules/vba/mod.rs index 23daf4980..f870fd32e 100644 --- a/lib/src/modules/vba/mod.rs +++ b/lib/src/modules/vba/mod.rs @@ -27,8 +27,7 @@ impl<'a> VbaExtractor<'a> { } fn is_zip(&self) -> bool { - let result = self.data.starts_with(&[0x50, 0x4B, 0x03, 0x04]); - result + self.data.starts_with(&[0x50, 0x4B, 0x03, 0x04]) } fn read_stream(&self, ole_parser: &crate::modules::olecf::parser::OLECFParser, name: &str) -> Result, &'static str> { @@ -45,7 +44,7 @@ impl<'a> VbaExtractor<'a> { } fn extract_from_ole(&self) -> Result { - let ole_parser = crate::modules::olecf::parser::OLECFParser::new(&self.data)?; + let ole_parser = crate::modules::olecf::parser::OLECFParser::new(self.data)?; let stream_names = ole_parser.get_stream_names()?; let mut vba_dir = None; @@ -54,11 +53,8 @@ impl<'a> VbaExtractor<'a> { // First process the dir stream if let Some(dir_name) = stream_names.iter().find(|n| n.to_lowercase().trim() == "dir") { - match self.read_stream(&ole_parser, dir_name) { - Ok(data) => { - vba_dir = Some(data); - }, - Err(_) => (), + if let Ok(data) = self.read_stream(&ole_parser, dir_name) { + vba_dir = Some(data); } } @@ -125,13 +121,10 @@ impl<'a> VbaExtractor<'a> { let _stream_size = ole_parser.get_stream_size(stream_name)?; if stream_name.starts_with("dir") { - match self.read_stream(&ole_parser, stream_name) { - Ok(data) => { - if !data.is_empty() { - vba_dir = Some(data); - } - }, - Err(_) => (), + if let Ok(data) = self.read_stream(&ole_parser, stream_name) { + if !data.is_empty() { + vba_dir = Some(data); + } } } } @@ -178,7 +171,7 @@ fn main(data: &[u8], _meta: Option<&[u8]>) -> Vba { let mut project_info = ProjectInfo::new(); project_info.name = Some(project.info.name.clone()); project_info.version = Some(project.info.version.clone()); - project_info.references = project.info.references.clone(); + project_info.references.clone_from(&project.info.references); // Add metadata let module_count = project.modules.len() as i32; diff --git a/lib/src/modules/vba/parser.rs b/lib/src/modules/vba/parser.rs index f65c48bba..7cf40677c 100644 --- a/lib/src/modules/vba/parser.rs +++ b/lib/src/modules/vba/parser.rs @@ -109,8 +109,7 @@ impl VbaProject { let length = (copy_token & length_mask) + 3; let temp1 = copy_token & offset_mask; let temp2 = 16 - bit_count; - let offset = u16::try_from((temp1 >> temp2) + 1) - .map_err(|_| "Offset calculation overflow")?; + let offset = (temp1 >> temp2) + 1; if offset as usize > decompressed.len() { return Err("Invalid copy token offset"); @@ -143,7 +142,7 @@ impl VbaProject { .map_err(|_nom_err| "Failed to parse u32") } - fn parse_bytes<'a>(input: &'a [u8], len: usize) -> Result<(&'a [u8], &'a [u8]), &'static str> { + fn parse_bytes(input: &[u8], len: usize) -> Result<(&[u8], &[u8]), &'static str> { if input.len() < len { Err("Not enough bytes to parse the requested slice") } else { @@ -214,7 +213,7 @@ impl VbaProject { } let (rest, name_size) = Self::parse_u32(_input)?; _input = rest; let name_size = name_size as usize; - if name_size < 1 || name_size > 128 { + if !(1..=128).contains(&name_size) { return Err("Project name not in valid range"); } let (rest, name_bytes) = Self::parse_bytes(rest, name_size)?; From ace9f2d7de0f06381491ed12be7b9afa6f546408 Mon Sep 17 00:00:00 2001 From: "Victor M. Alvarez" Date: Tue, 14 Jan 2025 13:27:22 +0100 Subject: [PATCH 05/12] style: fix clippy warning --- lib/src/modules/olecf/mod.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/src/modules/olecf/mod.rs b/lib/src/modules/olecf/mod.rs index 18d18dadf..805c2bfac 100644 --- a/lib/src/modules/olecf/mod.rs +++ b/lib/src/modules/olecf/mod.rs @@ -25,21 +25,18 @@ fn main(data: &[u8], _meta: Option<&[u8]>) -> Olecf { olecf.is_olecf = Some(is_valid); // Get stream names and sizes - match parser.get_stream_names() { - Ok(names) => { - // Get sizes for each stream - olecf.stream_sizes = names.iter() - .filter_map(|name| { - parser.get_stream_size(name) - .ok() - .map(|size| size as i64) - }) - .collect(); - - // Assign names last after we're done using them - olecf.stream_names = names; - }, - Err(_) => (), + if let Ok(names) = parser.get_stream_names() { + // Get sizes for each stream + olecf.stream_sizes = names.iter() + .filter_map(|name| { + parser.get_stream_size(name) + .ok() + .map(|size| size as i64) + }) + .collect(); + + // Assign names last after we're done using them + olecf.stream_names = names; } olecf From 0c0f4ec1006dfe0a3cdd9dd18c50714999466321 Mon Sep 17 00:00:00 2001 From: "Victor M. Alvarez" Date: Mon, 20 Jan 2025 11:46:20 +0100 Subject: [PATCH 06/12] chore: remove `println` --- lib/src/modules/vba/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/modules/vba/mod.rs b/lib/src/modules/vba/mod.rs index f870fd32e..51dbed6a5 100644 --- a/lib/src/modules/vba/mod.rs +++ b/lib/src/modules/vba/mod.rs @@ -71,7 +71,6 @@ impl<'a> VbaExtractor<'a> { if let Ok(data) = self.read_stream(&ole_parser, name) { if !data.is_empty() { modules.insert(name.clone(), data); - println!("Added module: {}", name); } } } else if lowercase_name.contains("project") && !lowercase_name.contains("_vba_project") { From 12466494b2ca21038d2832cd3944971df3ef3a4e Mon Sep 17 00:00:00 2001 From: "Victor M. Alvarez" Date: Mon, 20 Jan 2025 11:47:36 +0100 Subject: [PATCH 07/12] style: apply `rustfmt` --- lib/src/modules/olecf/parser.rs | 24 ++++---- lib/src/modules/vba/mod.rs | 102 ++++++++++++++++++-------------- 2 files changed, 71 insertions(+), 55 deletions(-) diff --git a/lib/src/modules/olecf/parser.rs b/lib/src/modules/olecf/parser.rs index acd0ab373..eb2bb2b35 100644 --- a/lib/src/modules/olecf/parser.rs +++ b/lib/src/modules/olecf/parser.rs @@ -77,7 +77,7 @@ impl<'a> OLECFParser<'a> { } - fn parse_header(&mut self, input: &'a [u8]) -> IResult<&'a [u8], ()> { + fn parse_header(&mut self, input: &'a [u8]) -> IResult<&'a [u8], ()> { let (mut input, ( _skip_20, byte_order, @@ -101,12 +101,12 @@ impl<'a> OLECFParser<'a> { le_u32, // parse _first_difat_sector le_u32, // parse _difat_count ))(input)?; - + // (A) Verify `byte_order == 0xFFFE`. if byte_order != 0xFFFE { return Err(nom::Err::Error(NomError::new(input, ErrorKind::Verify))); } - + // (B) Parse up to 109 DIFAT entries from `input` // 109 is the max allowed number of DIFAT entries in the header. let rest = input; @@ -128,27 +128,27 @@ impl<'a> OLECFParser<'a> { self.fat_sectors.append(&mut filtered); input = rest2; } - + // (C) Directory chain if first_dir_sector < MAX_REGULAR_SECTOR { self.directory_sectors = self.follow_chain(first_dir_sector); } else { return Err(nom::Err::Error(NomError::new(input, ErrorKind::Verify))); } - + // (D) MiniFAT chain if mini_fat_count > 0 && first_mini_fat < MAX_REGULAR_SECTOR { self.mini_fat_sectors = self.follow_chain(first_mini_fat); } - + // (E) If no FAT sectors but num_fat_sectors != 0 => error if self.fat_sectors.is_empty() && num_fat_sectors > 0 { return Err(nom::Err::Error(NomError::new(input, ErrorKind::Verify))); } - + Ok((input, ())) } - + fn parse_directory(&mut self, _input: &'a [u8]) -> IResult<&'a [u8], ()> { if self.directory_sectors.is_empty() { @@ -249,19 +249,19 @@ impl<'a> OLECFParser<'a> { // We've seen this sector before - it's a cycle break; } - + chain.push(current); - + let next = match self.get_fat_entry(current) { Ok(n) => n, Err(_) => break, }; - + // Check validity of next sector if next >= MAX_REGULAR_SECTOR || next == FREESECT || next == ENDOFCHAIN { break; } - + current = next; } chain diff --git a/lib/src/modules/vba/mod.rs b/lib/src/modules/vba/mod.rs index 51dbed6a5..b40232ebd 100644 --- a/lib/src/modules/vba/mod.rs +++ b/lib/src/modules/vba/mod.rs @@ -1,25 +1,25 @@ -/*! YARA module that extracts VBA (Visual Basic for Applications) macros from Office documents. +/*! YARA module that extracts VBA (Visual Basic for Applications) macros from Office documents. Read more about the VBA file format specification here: https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/575462ba-bf67-4190-9fac-c275523c75fc */ use crate::modules::prelude::*; -use crate::modules::protos::vba::*; use crate::modules::protos::vba::vba::ProjectInfo; +use crate::modules::protos::vba::*; use protobuf::MessageField; use std::collections::HashMap; -use std::io::Read; use std::io::Cursor; +use std::io::Read; use zip::ZipArchive; mod parser; -use parser::{VbaProject, ModuleType}; +use parser::{ModuleType, VbaProject}; #[derive(Debug)] struct VbaExtractor<'a> { data: &'a [u8], - } +} impl<'a> VbaExtractor<'a> { fn new(data: &'a [u8]) -> Self { @@ -30,57 +30,67 @@ impl<'a> VbaExtractor<'a> { self.data.starts_with(&[0x50, 0x4B, 0x03, 0x04]) } - fn read_stream(&self, ole_parser: &crate::modules::olecf::parser::OLECFParser, name: &str) -> Result, &'static str> { + fn read_stream( + &self, + ole_parser: &crate::modules::olecf::parser::OLECFParser, + name: &str, + ) -> Result, &'static str> { let size = ole_parser.get_stream_size(name)? as usize; - + // Skip empty streams if size == 0 { return Err("Stream is empty"); } - + let data = ole_parser.get_stream_data(name)?; - + Ok(data) } fn extract_from_ole(&self) -> Result { - let ole_parser = crate::modules::olecf::parser::OLECFParser::new(self.data)?; + let ole_parser = + crate::modules::olecf::parser::OLECFParser::new(self.data)?; let stream_names = ole_parser.get_stream_names()?; - + let mut vba_dir = None; let mut modules = HashMap::new(); let mut project_streams = Vec::new(); - + // First process the dir stream - if let Some(dir_name) = stream_names.iter().find(|n| n.to_lowercase().trim() == "dir") { + if let Some(dir_name) = + stream_names.iter().find(|n| n.to_lowercase().trim() == "dir") + { if let Ok(data) = self.read_stream(&ole_parser, dir_name) { vba_dir = Some(data); } } - + // Then process other streams for name in &stream_names { let lowercase_name = name.to_lowercase(); - + if lowercase_name != "dir" { - if lowercase_name.contains("module") || - lowercase_name.contains("thisdocument") || - lowercase_name.ends_with(".bas") || - lowercase_name.ends_with(".cls") || - lowercase_name.ends_with(".frm") { + if lowercase_name.contains("module") + || lowercase_name.contains("thisdocument") + || lowercase_name.ends_with(".bas") + || lowercase_name.ends_with(".cls") + || lowercase_name.ends_with(".frm") + { if let Ok(data) = self.read_stream(&ole_parser, name) { if !data.is_empty() { modules.insert(name.clone(), data); } } - } else if lowercase_name.contains("project") && !lowercase_name.contains("_vba_project") { + } else if lowercase_name.contains("project") + && !lowercase_name.contains("_vba_project") + { if let Ok(data) = self.read_stream(&ole_parser, name) { project_streams.push(data); } } } } - + // Always try the dir stream first if we found it if let Some(dir_data) = vba_dir { parser::VbaProject::parse(&dir_data, modules) @@ -88,46 +98,52 @@ impl<'a> VbaExtractor<'a> { Err("No VBA directory stream found") } } - + fn extract_from_zip(&self) -> Result { let reader = Cursor::new(&self.data); let mut archive = ZipArchive::new(reader) .map_err(|_| "Failed to read ZIP archive")?; - + // Search for potential VBA project files let vba_project_names = [ "word/vbaProject.bin", "xl/vbaProject.bin", "ppt/vbaProject.bin", - "vbaProject.bin" + "vbaProject.bin", ]; - + for name in &vba_project_names { match archive.by_name(name) { Ok(mut file) => { let mut contents = Vec::new(); file.read_to_end(&mut contents) .map_err(|_| "Failed to read vbaProject.bin")?; - + // Parse as OLE - let ole_parser = crate::modules::olecf::parser::OLECFParser::new(&contents)?; + let ole_parser = + crate::modules::olecf::parser::OLECFParser::new( + &contents, + )?; let stream_names = ole_parser.get_stream_names()?; - + let mut vba_dir = None; let mut modules = HashMap::new(); - + for stream_name in &stream_names { - let _stream_size = ole_parser.get_stream_size(stream_name)?; + let _stream_size = + ole_parser.get_stream_size(stream_name)?; if stream_name.starts_with("dir") { - if let Ok(data) = self.read_stream(&ole_parser, stream_name) { + if let Ok(data) = + self.read_stream(&ole_parser, stream_name) + { if !data.is_empty() { vba_dir = Some(data); } } } } - + // Process other streams for name in &stream_names { if let Ok(data) = self.read_stream(&ole_parser, name) { @@ -136,16 +152,16 @@ impl<'a> VbaExtractor<'a> { } } } - + // Use dir stream if found, otherwise fail if let Some(dir_data) = vba_dir { return parser::VbaProject::parse(&dir_data, modules); } - }, + } Err(_) => continue, } } - + Err("No VBA project found in ZIP") } } @@ -154,9 +170,9 @@ impl<'a> VbaExtractor<'a> { fn main(data: &[u8], _meta: Option<&[u8]>) -> Vba { let mut vba = Vba::new(); vba.has_macros = Some(false); - + let extractor = VbaExtractor::new(data); - + let project_result = if extractor.is_zip() { extractor.extract_from_zip() } else { @@ -166,17 +182,17 @@ fn main(data: &[u8], _meta: Option<&[u8]>) -> Vba { match project_result { Ok(project) => { vba.has_macros = Some(true); - + let mut project_info = ProjectInfo::new(); project_info.name = Some(project.info.name.clone()); project_info.version = Some(project.info.version.clone()); project_info.references.clone_from(&project.info.references); - + // Add metadata let module_count = project.modules.len() as i32; project_info.module_count = Some(module_count); project_info.is_compressed = Some(true); - + vba.project_info = MessageField::some(project_info); // Process modules @@ -189,11 +205,11 @@ fn main(data: &[u8], _meta: Option<&[u8]>) -> Vba { }); vba.module_codes.push(module.code.clone()); } - }, + } Err(_) => { vba.has_macros = Some(false); } } vba -} \ No newline at end of file +} From 7f5348093f6cf2f684eca2e3798106b8b9f6438e Mon Sep 17 00:00:00 2001 From: "Victor M. Alvarez" Date: Mon, 20 Jan 2025 13:35:05 +0100 Subject: [PATCH 08/12] style: apply `rustfmt` --- lib/src/modules/olecf/mod.rs | 29 +++++---- lib/src/modules/olecf/parser.rs | 112 ++++++++++++++++++++++---------- 2 files changed, 94 insertions(+), 47 deletions(-) diff --git a/lib/src/modules/olecf/mod.rs b/lib/src/modules/olecf/mod.rs index 805c2bfac..660d5c8f8 100644 --- a/lib/src/modules/olecf/mod.rs +++ b/lib/src/modules/olecf/mod.rs @@ -1,12 +1,12 @@ /*! YARA module that parses OLE Compound File Binary Format files. -The OLE CF format (also known as Compound File Binary Format or CFBF) is a +The OLE CF format (also known as Compound File Binary Format or CFBF) is a container format used by many Microsoft file formats including DOC, XLS, PPT, -and MSI. This module specializes in parsing OLE CF files and extracting +and MSI. This module specializes in parsing OLE CF files and extracting metadata about their structure and contents. -Read more about the Compound File Binary File format here: - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b +Read more about the Compound File Binary File format here: +https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b */ use crate::modules::prelude::*; @@ -15,36 +15,37 @@ pub mod parser; #[module_main] fn main(data: &[u8], _meta: Option<&[u8]>) -> Olecf { - match parser::OLECFParser::new(data) { Ok(parser) => { let mut olecf = Olecf::new(); - + // Check and set is_olecf let is_valid = parser.is_valid_header(); olecf.is_olecf = Some(is_valid); - + // Get stream names and sizes - if let Ok(names) = parser.get_stream_names() { + if let Ok(names) = parser.get_stream_names() { // Get sizes for each stream - olecf.stream_sizes = names.iter() + olecf.stream_sizes = names + .iter() .filter_map(|name| { - parser.get_stream_size(name) + parser + .get_stream_size(name) .ok() .map(|size| size as i64) }) .collect(); - + // Assign names last after we're done using them olecf.stream_names = names; } - + olecf - }, + } Err(_) => { let mut olecf = Olecf::new(); olecf.is_olecf = Some(false); olecf } } -} \ No newline at end of file +} diff --git a/lib/src/modules/olecf/parser.rs b/lib/src/modules/olecf/parser.rs index eb2bb2b35..6d15db826 100644 --- a/lib/src/modules/olecf/parser.rs +++ b/lib/src/modules/olecf/parser.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use nom::{ bytes::complete::take, combinator::verify, @@ -8,8 +7,10 @@ use nom::{ sequence::tuple, IResult, }; +use std::collections::HashMap; -const OLECF_SIGNATURE: &[u8] = &[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]; +const OLECF_SIGNATURE: &[u8] = + &[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]; const SECTOR_SHIFT: u16 = 9; const MINI_SECTOR_SHIFT: u16 = 6; const DIRECTORY_ENTRY_SIZE: u64 = 128; @@ -104,7 +105,10 @@ impl<'a> OLECFParser<'a> { // (A) Verify `byte_order == 0xFFFE`. if byte_order != 0xFFFE { - return Err(nom::Err::Error(NomError::new(input, ErrorKind::Verify))); + return Err(nom::Err::Error(NomError::new( + input, + ErrorKind::Verify, + ))); } // (B) Parse up to 109 DIFAT entries from `input` @@ -133,7 +137,10 @@ impl<'a> OLECFParser<'a> { if first_dir_sector < MAX_REGULAR_SECTOR { self.directory_sectors = self.follow_chain(first_dir_sector); } else { - return Err(nom::Err::Error(NomError::new(input, ErrorKind::Verify))); + return Err(nom::Err::Error(NomError::new( + input, + ErrorKind::Verify, + ))); } // (D) MiniFAT chain @@ -143,24 +150,32 @@ impl<'a> OLECFParser<'a> { // (E) If no FAT sectors but num_fat_sectors != 0 => error if self.fat_sectors.is_empty() && num_fat_sectors > 0 { - return Err(nom::Err::Error(NomError::new(input, ErrorKind::Verify))); + return Err(nom::Err::Error(NomError::new( + input, + ErrorKind::Verify, + ))); } Ok((input, ())) } - fn parse_directory(&mut self, _input: &'a [u8]) -> IResult<&'a [u8], ()> { if self.directory_sectors.is_empty() { - return Err(nom::Err::Error(NomError::new(_input, ErrorKind::Verify))); + return Err(nom::Err::Error(NomError::new( + _input, + ErrorKind::Verify, + ))); } for §or in &self.directory_sectors { let mut entry_offset = 0; - while entry_offset + DIRECTORY_ENTRY_SIZE as usize <= self.sector_size { + while entry_offset + DIRECTORY_ENTRY_SIZE as usize + <= self.sector_size + { let abs_offset = self.sector_to_offset(sector) + entry_offset; - if abs_offset + DIRECTORY_ENTRY_SIZE as usize > self.data.len() { + if abs_offset + DIRECTORY_ENTRY_SIZE as usize > self.data.len() + { break; } if let Ok(entry) = self.read_directory_entry(abs_offset) { @@ -194,14 +209,22 @@ impl<'a> OLECFParser<'a> { Ok(self.dir_entries.keys().cloned().collect()) } - pub fn get_stream_size(&self, stream_name: &str) -> Result { - self.dir_entries.get(stream_name).map(|e| e.size).ok_or("Stream not found") + pub fn get_stream_size( + &self, + stream_name: &str, + ) -> Result { + self.dir_entries + .get(stream_name) + .map(|e| e.size) + .ok_or("Stream not found") } - pub fn get_stream_data(&self, stream_name: &str) -> Result, &'static str> { - let entry = self.dir_entries - .get(stream_name) - .ok_or("Stream not found")?; + pub fn get_stream_data( + &self, + stream_name: &str, + ) -> Result, &'static str> { + let entry = + self.dir_entries.get(stream_name).ok_or("Stream not found")?; if entry.size < 4096 && entry.stream_type != ROOT_STORAGE_TYPE { self.get_mini_stream_data(entry.start_sector, entry.size) @@ -258,7 +281,10 @@ impl<'a> OLECFParser<'a> { }; // Check validity of next sector - if next >= MAX_REGULAR_SECTOR || next == FREESECT || next == ENDOFCHAIN { + if next >= MAX_REGULAR_SECTOR + || next == FREESECT + || next == ENDOFCHAIN + { break; } @@ -267,7 +293,10 @@ impl<'a> OLECFParser<'a> { chain } - fn read_directory_entry(&self, offset: usize) -> Result { + fn read_directory_entry( + &self, + offset: usize, + ) -> Result { if offset + 128 > self.data.len() { return Err("Incomplete directory entry"); } @@ -278,7 +307,8 @@ impl<'a> OLECFParser<'a> { } let name_bytes = &self.data[offset..offset + name_len]; - let filtered: Vec = name_bytes.iter().copied().filter(|&b| b != 0).collect(); + let filtered: Vec = + name_bytes.iter().copied().filter(|&b| b != 0).collect(); let name = String::from_utf8_lossy(&filtered).to_string(); let stream_type = self.data[offset + 66]; @@ -286,22 +316,23 @@ impl<'a> OLECFParser<'a> { let size_32 = parse_u32_at(self.data, offset + 120)?; let size = size_32 as u64; - Ok(DirectoryEntry { - name, - size, - start_sector, - stream_type, - }) + Ok(DirectoryEntry { name, size, start_sector, stream_type }) } - fn get_regular_stream_data(&self, start_sector: u32, size: u64) -> Result, &'static str> { + fn get_regular_stream_data( + &self, + start_sector: u32, + size: u64, + ) -> Result, &'static str> { let mut data = Vec::with_capacity(size as usize); let mut current_sector = start_sector; let mut total_read = 0; - while current_sector < MAX_REGULAR_SECTOR && total_read < size as usize { + while current_sector < MAX_REGULAR_SECTOR && total_read < size as usize + { let sector_data = self.read_sector(current_sector)?; - let bytes_to_read = std::cmp::min(self.sector_size, size as usize - total_read); + let bytes_to_read = + std::cmp::min(self.sector_size, size as usize - total_read); data.extend_from_slice(§or_data[..bytes_to_read]); total_read += bytes_to_read; @@ -323,10 +354,16 @@ impl<'a> OLECFParser<'a> { } fn get_root_mini_stream_data(&self) -> Result, &'static str> { - self.get_regular_stream_data(self.mini_stream_start, self.mini_stream_size) + self.get_regular_stream_data( + self.mini_stream_start, + self.mini_stream_size, + ) } - fn get_minifat_entry(&self, mini_sector: u32) -> Result { + fn get_minifat_entry( + &self, + mini_sector: u32, + ) -> Result { if self.mini_fat_sectors.is_empty() { return Ok(ENDOFCHAIN); } @@ -343,7 +380,11 @@ impl<'a> OLECFParser<'a> { parse_u32_at(fat, offset) } - fn get_mini_stream_data(&self, start_mini_sector: u32, size: u64) -> Result, &'static str> { + fn get_mini_stream_data( + &self, + start_mini_sector: u32, + size: u64, + ) -> Result, &'static str> { if self.mini_stream_size == 0 { return Err("No mini stream present"); } @@ -360,12 +401,17 @@ impl<'a> OLECFParser<'a> { return Err("Mini stream offset out of range"); } - let bytes_to_read = std::cmp::min(self.mini_sector_size, size as usize - data.len()); + let bytes_to_read = std::cmp::min( + self.mini_sector_size, + size as usize - data.len(), + ); if mini_offset + bytes_to_read > mini_data_len { return Err("Mini stream extends beyond available data"); } - data.extend_from_slice(&mini_stream_data[mini_offset..mini_offset + bytes_to_read]); + data.extend_from_slice( + &mini_stream_data[mini_offset..mini_offset + bytes_to_read], + ); if data.len() < size as usize { let next = self.get_minifat_entry(current)?; @@ -404,4 +450,4 @@ fn parse_u32_at(data: &[u8], offset: usize) -> Result { Ok((_, val)) => Ok(val), Err(_) => Err("Failed to parse u32"), } -} \ No newline at end of file +} From 123bf3164132302b1f5a3178d96ccb1070caa0f8 Mon Sep 17 00:00:00 2001 From: "Victor M. Alvarez" Date: Mon, 20 Jan 2025 13:57:30 +0100 Subject: [PATCH 09/12] refactor: some changes to make `parse_header` easier to follow. --- lib/src/modules/olecf/parser.rs | 83 +++++++++++++++++---------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/lib/src/modules/olecf/parser.rs b/lib/src/modules/olecf/parser.rs index 6d15db826..0e3171e91 100644 --- a/lib/src/modules/olecf/parser.rs +++ b/lib/src/modules/olecf/parser.rs @@ -65,52 +65,55 @@ impl<'a> OLECFParser<'a> { } fn parse(&mut self, input: &'a [u8]) -> IResult<&'a [u8], ()> { - // (A) Check the 8-byte OLECF signature. - let (input, _) = verify(take(8_usize), |sig: &[u8]| sig == OLECF_SIGNATURE)(input)?; - - // (B) Parse the rest of the header fields. let (input, ()) = self.parse_header(input)?; - - // (C) Parse the directory chain. - let (input, ()) = self.parse_directory(input)?; - - Ok((input, ())) + self.parse_directory(input) } - + /// Parses the Compound File Header. + /// + /// [MS-CFB] Section 2.2 fn parse_header(&mut self, input: &'a [u8]) -> IResult<&'a [u8], ()> { - let (mut input, ( - _skip_20, - byte_order, - _skip_14, - num_fat_sectors, - first_dir_sector, - _skip_8, - first_mini_fat, - mini_fat_count, - _first_difat_sector, - _difat_count, - )) = tuple(( - take(20usize), // skip 20 bytes - le_u16, // parse byte_order - take(14usize), // skip 14 bytes - le_u32, // parse num_fat_sectors - le_u32, // parse first_dir_sector - take(8usize), // skip 8 bytes - le_u32, // parse first_mini_fat - le_u32, // parse mini_fat_count - le_u32, // parse _first_difat_sector - le_u32, // parse _difat_count + let ( + mut input, + ( + _signature, + _clsid, + _minor_version, + _major_version, + _byte_order, + _sector_shift, + _mini_sector_shift, + _reserved, + _num_dir_sectors, + num_fat_sectors, + first_dir_sector, + _transaction_sig_num, + _mini_stream_cutoff_size, + first_mini_fat, + mini_fat_count, + _first_difat_sector, + _difat_count, + ), + ) = tuple(( + verify(take(8_usize), |sig: &[u8]| sig == OLECF_SIGNATURE), + take(16usize), // CLSID, + le_u16, // minor_version + le_u16, // major_version + verify(le_u16, |byte_order| *byte_order == 0xFFFE), + le_u16, // sector_shift + le_u16, // mini_sector_shift + take(6usize), // reserved + le_u32, // num_dir_sectors + le_u32, // num_fat_sectors + le_u32, // first_dir_sector + le_u32, // transaction_sig_num + le_u32, // mini_stream_cutoff_size + le_u32, // first_mini_fat + le_u32, // mini_fat_count + le_u32, // _first_difat_sector + le_u32, // _difat_count ))(input)?; - // (A) Verify `byte_order == 0xFFFE`. - if byte_order != 0xFFFE { - return Err(nom::Err::Error(NomError::new( - input, - ErrorKind::Verify, - ))); - } - // (B) Parse up to 109 DIFAT entries from `input` // 109 is the max allowed number of DIFAT entries in the header. let rest = input; From bd7cff20c56cf2cbc1cb506a75269ae0300ea0d7 Mon Sep 17 00:00:00 2001 From: "Victor M. Alvarez" Date: Tue, 4 Feb 2025 10:27:45 +0100 Subject: [PATCH 10/12] refactor: simplify the parsing of the DIFAT entries after the header. --- lib/src/modules/olecf/parser.rs | 38 +++++++++++++-------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/lib/src/modules/olecf/parser.rs b/lib/src/modules/olecf/parser.rs index 0e3171e91..d0b081ef3 100644 --- a/lib/src/modules/olecf/parser.rs +++ b/lib/src/modules/olecf/parser.rs @@ -1,8 +1,8 @@ +use nom::multi::fold_many_m_n; use nom::{ bytes::complete::take, combinator::verify, error::{Error as NomError, ErrorKind}, - multi::count, number::complete::{le_u16, le_u32}, sequence::tuple, IResult, @@ -74,7 +74,7 @@ impl<'a> OLECFParser<'a> { /// [MS-CFB] Section 2.2 fn parse_header(&mut self, input: &'a [u8]) -> IResult<&'a [u8], ()> { let ( - mut input, + input, ( _signature, _clsid, @@ -114,27 +114,19 @@ impl<'a> OLECFParser<'a> { le_u32, // _difat_count ))(input)?; - // (B) Parse up to 109 DIFAT entries from `input` - // 109 is the max allowed number of DIFAT entries in the header. - let rest = input; - if rest.len() < 109 * 4 { - let possible = rest.len() / 4; - let (rest2, entries) = count(le_u32, possible)(rest)?; - let mut filtered = entries - .into_iter() - .filter(|&x| x < MAX_REGULAR_SECTOR) - .collect::>(); - self.fat_sectors.append(&mut filtered); - input = rest2; - } else { - let (rest2, entries) = count(le_u32, 109)(rest)?; - let mut filtered = entries - .into_iter() - .filter(|&x| x < MAX_REGULAR_SECTOR) - .collect::>(); - self.fat_sectors.append(&mut filtered); - input = rest2; - } + // Parse the first 109 DIFAT entries, which are contained in the + // header sector. + let (input, _) = fold_many_m_n( + 0, + 109, + le_u32, + || {}, + |_, sector| { + if sector < MAX_REGULAR_SECTOR { + self.fat_sectors.push(sector); + } + }, + )(input)?; // (C) Directory chain if first_dir_sector < MAX_REGULAR_SECTOR { From cc2241284b36841c087b7c0cdf5f8b7365db765e Mon Sep 17 00:00:00 2001 From: "Victor M. Alvarez" Date: Tue, 4 Feb 2025 11:37:13 +0100 Subject: [PATCH 11/12] refactor: simplify `follow_chain` function. --- lib/src/modules/olecf/parser.rs | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/lib/src/modules/olecf/parser.rs b/lib/src/modules/olecf/parser.rs index d0b081ef3..94ddfcaea 100644 --- a/lib/src/modules/olecf/parser.rs +++ b/lib/src/modules/olecf/parser.rs @@ -22,7 +22,6 @@ const ROOT_STORAGE_TYPE: u8 = 5; // Special sectors const ENDOFCHAIN: u32 = 0xFFFFFFFE; -const FREESECT: u32 = 0xFFFFFFFF; const MAX_REGULAR_SECTOR: u32 = 0xFFFFFFFA; pub struct OLECFParser<'a> { @@ -256,12 +255,14 @@ impl<'a> OLECFParser<'a> { fn follow_chain(&self, start_sector: u32) -> Vec { let mut chain = Vec::new(); - if start_sector >= MAX_REGULAR_SECTOR { - return chain; - } - let mut current = start_sector; - while current < MAX_REGULAR_SECTOR { + + loop { + // Ensure that the current sector is a valid one. + if current > MAX_REGULAR_SECTOR { + break; + } + // Prevent cycles by keeping track of visited sectors if chain.contains(¤t) { // We've seen this sector before - it's a cycle @@ -270,21 +271,14 @@ impl<'a> OLECFParser<'a> { chain.push(current); - let next = match self.get_fat_entry(current) { - Ok(n) => n, + // Now current is the next entry in the chain. + current = match self.get_fat_entry(current) { Err(_) => break, + Ok(n) if n == ENDOFCHAIN => break, + Ok(n) => n, }; - - // Check validity of next sector - if next >= MAX_REGULAR_SECTOR - || next == FREESECT - || next == ENDOFCHAIN - { - break; - } - - current = next; } + chain } From ba1f2dd63614bdbd7feb7bd938649e3be5e989c1 Mon Sep 17 00:00:00 2001 From: "Victor M. Alvarez" Date: Tue, 4 Feb 2025 16:26:47 +0100 Subject: [PATCH 12/12] refactor: put stream names and sizes under a structure that represents a stream. --- lib/src/modules/olecf/mod.rs | 43 +++++++++++------------------- lib/src/modules/olecf/parser.rs | 19 ++++++++----- lib/src/modules/protos/olecf.proto | 13 ++++----- 3 files changed, 36 insertions(+), 39 deletions(-) diff --git a/lib/src/modules/olecf/mod.rs b/lib/src/modules/olecf/mod.rs index 660d5c8f8..298a03319 100644 --- a/lib/src/modules/olecf/mod.rs +++ b/lib/src/modules/olecf/mod.rs @@ -11,41 +11,30 @@ https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b use crate::modules::prelude::*; use crate::modules::protos::olecf::*; + pub mod parser; #[module_main] fn main(data: &[u8], _meta: Option<&[u8]>) -> Olecf { + let mut olecf = Olecf::new(); + match parser::OLECFParser::new(data) { Ok(parser) => { - let mut olecf = Olecf::new(); - - // Check and set is_olecf - let is_valid = parser.is_valid_header(); - olecf.is_olecf = Some(is_valid); - - // Get stream names and sizes - if let Ok(names) = parser.get_stream_names() { - // Get sizes for each stream - olecf.stream_sizes = names - .iter() - .filter_map(|name| { - parser - .get_stream_size(name) - .ok() - .map(|size| size as i64) - }) - .collect(); - - // Assign names last after we're done using them - olecf.stream_names = names; - } - - olecf + olecf.set_is_olecf(parser.is_valid_header()); + olecf.streams = parser + .get_streams() + .map(|(name, entry)| { + let mut s = Stream::new(); + s.set_name(name.to_string()); + s.set_size(entry.size); + s + }) + .collect(); } Err(_) => { - let mut olecf = Olecf::new(); - olecf.is_olecf = Some(false); - olecf + olecf.set_is_olecf(false); } } + + olecf } diff --git a/lib/src/modules/olecf/parser.rs b/lib/src/modules/olecf/parser.rs index 94ddfcaea..21e0074b0 100644 --- a/lib/src/modules/olecf/parser.rs +++ b/lib/src/modules/olecf/parser.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use nom::multi::fold_many_m_n; use nom::{ bytes::complete::take, @@ -7,7 +9,6 @@ use nom::{ sequence::tuple, IResult, }; -use std::collections::HashMap; const OLECF_SIGNATURE: &[u8] = &[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]; @@ -36,11 +37,11 @@ pub struct OLECFParser<'a> { mini_stream_size: u64, } -struct DirectoryEntry { - name: String, - size: u64, - start_sector: u32, - stream_type: u8, +pub struct DirectoryEntry { + pub name: String, + pub size: u64, + pub start_sector: u32, + pub stream_type: u8, } impl<'a> OLECFParser<'a> { @@ -213,6 +214,12 @@ impl<'a> OLECFParser<'a> { .ok_or("Stream not found") } + pub fn get_streams( + &self, + ) -> impl Iterator { + self.dir_entries.iter().map(|(name, entry)| (name.as_str(), entry)) + } + pub fn get_stream_data( &self, stream_name: &str, diff --git a/lib/src/modules/protos/olecf.proto b/lib/src/modules/protos/olecf.proto index 6b42b3a3a..4dd56724a 100644 --- a/lib/src/modules/protos/olecf.proto +++ b/lib/src/modules/protos/olecf.proto @@ -11,12 +11,13 @@ option (yara.module_options) = { }; message Olecf { - // Check if file is an OLE CF file + // True if file is an OLE CF file. required bool is_olecf = 1; + // Streams contained in the OLE CF file. + repeated Stream streams = 2; +} - // Get array of stream names - repeated string stream_names = 2; - - // Get size of a specific stream by name - repeated int64 stream_sizes = 3; +message Stream { + required string name = 1; + required uint64 size = 2; } \ No newline at end of file