diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 75c77d0df..f7949c7a6 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -209,6 +209,12 @@ magic-module = [ # The `math` module. math-module = [] +# The `msi` module parses MSI files. +msi-module = [ + "olecf-module", + "crypto", +] + # The `olecf` module olecf-module = [ "dep:nom" @@ -234,6 +240,8 @@ time-module = [] # The `vba` module vba-module = [ + "olecf-module", + "zip-module", "dep:codepage", "dep:encoding_rs", "dep:nom", @@ -259,19 +267,18 @@ default-modules = [ "dex-module", "dotnet-module", "elf-module", + "lnk-module", "macho-module", "math-module", + "msi-module", "hash-module", - # Still experimental - # "olecf-module", + "olecf-module", "pe-module", "string-module", "time-module", - "lnk-module", - # Still experimental - # "vba-module", "test_proto2-module", "test_proto3-module", + "vba-module", "vt-module", "zip-module", ] diff --git a/lib/src/modules/add_modules.rs b/lib/src/modules/add_modules.rs index 292bf0149..1fac84308 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 = "msi-module")] +add_module!(modules, "msi", msi, "msi.Msi", Some("msi"), Some(msi::__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")] diff --git a/lib/src/modules/mod.rs b/lib/src/modules/mod.rs index c5038da11..2b8fc3254 100644 --- a/lib/src/modules/mod.rs +++ b/lib/src/modules/mod.rs @@ -41,8 +41,14 @@ pub enum ModuleError { #[derive(Default)] pub struct ModuleContext<'a> { module_metadata: FxHashMap<&'static str, &'a [u8]>, + #[cfg(any( + feature = "olecf-module", + feature = "msi-module", + feature = "vba-module" + ))] + pub(crate) olecf_cache: Option>, #[cfg(any(feature = "zip-module", feature = "vba-module"))] - pub(crate) zip_cache: Option>, + pub(crate) zip_cache: Option>, } impl<'a> ModuleContext<'a> { @@ -267,6 +273,11 @@ pub mod mods { /// Data structure returned by the `olecf` module. pub use super::protos::olecf::Olecf; + /// Data structures defined by the `msi` module. + pub use super::protos::msi; + /// Data structure returned by the `msi` module. + pub use super::protos::msi::Msi; + /// Data structures defined by the `vba` module. /// /// The main structure produced by the module is [`vba::Vba`]. The rest @@ -379,6 +390,7 @@ pub mod mods { info.vba = protobuf::MessageField(invoke::(data)); info.crx = protobuf::MessageField(invoke::(data)); info.dex = protobuf::MessageField(invoke::(data)); + info.msi = protobuf::MessageField(invoke::(data)); info } diff --git a/lib/src/modules/modules.rs b/lib/src/modules/modules.rs index 23c7f4bd2..a1299c298 100644 --- a/lib/src/modules/modules.rs +++ b/lib/src/modules/modules.rs @@ -21,6 +21,8 @@ mod macho; mod magic; #[cfg(feature = "math-module")] mod math; +#[cfg(feature = "msi-module")] +mod msi; #[cfg(feature = "olecf-module")] mod olecf; #[cfg(feature = "pe-module")] diff --git a/lib/src/modules/msi/mod.rs b/lib/src/modules/msi/mod.rs new file mode 100644 index 000000000..1184d62e6 --- /dev/null +++ b/lib/src/modules/msi/mod.rs @@ -0,0 +1,39 @@ +/*! YARA module that parses Microsoft Software Installer (MSI) files. + +MSI files are OLE Compound Files (CFB) containing installation packages. +This module specializes in validating digital signatures in MSI files. +*/ + +use crate::errors::ModuleError; +use crate::mods::prelude::*; +use crate::modules::protos::msi::*; +use crate::modules::utils::olecf::CachedOlecf; + +pub mod parser; + +fn main<'a>( + ctx: &mut ModuleContext<'a>, + data: &'a [u8], +) -> Result { + let cached = ctx.olecf_cache.get_or_insert_with(|| CachedOlecf::new(data)); + + let olecf = match cached { + CachedOlecf::Olecf(olecf) => olecf, + CachedOlecf::NotOlecf => { + let mut msi = Msi::new(); + msi.set_is_signed(false); + return Ok(msi); + } + }; + + match parser::Msi::parse(olecf) { + Ok(msi) => Ok(msi), + Err(_) => { + let mut msi = Msi::new(); + msi.set_is_signed(false); + Ok(msi) + } + } +} + +register_module!("msi", Msi, main); diff --git a/lib/src/modules/msi/parser.rs b/lib/src/modules/msi/parser.rs new file mode 100644 index 000000000..91ad75da9 --- /dev/null +++ b/lib/src/modules/msi/parser.rs @@ -0,0 +1,79 @@ +use crate::modules::olecf::parser::Olecf; +use crate::modules::protos; +use crate::modules::protos::msi::Msi as MsiProto; +use crate::modules::utils::authenticode::{ + AuthenticodeHasher, AuthenticodeParser, +}; + +pub struct Msi; + +struct MsiHasher<'a> { + olecf: &'a Olecf<'a>, +} + +impl AuthenticodeHasher for MsiHasher<'_> { + fn hash(&self, digest: &mut dyn digest::Update) -> Option<()> { + let data = self.olecf.data(); + let sector_size = self.olecf.sector_size(); + if data.len() < sector_size { + return None; + } + + // 1. CFB Header: + // First sector_size bytes, with CLSID (bytes 8..24) + // and State Bits (bytes 24..28) zeroed out. + let header_bytes = &data[..sector_size]; + if header_bytes.len() >= 28 { + digest.update(&header_bytes[..8]); + digest.update(&[0u8; 20]); + digest.update(&header_bytes[28..]); + } else { + digest.update(header_bytes); + } + + // 2. Stream contents: + // Hash stream data for all streams except DigitalSignature and MsiDigitalSignatureEx. + for (name, _) in self.olecf.streams() { + let clean_name = name.trim_start_matches(|c: char| c < '\u{20}'); + if clean_name.eq_ignore_ascii_case("DigitalSignature") + || clean_name.eq_ignore_ascii_case("MsiDigitalSignatureEx") + { + continue; + } + if let Ok(stream_data) = self.olecf.get_stream_data(name) { + digest.update(&stream_data); + } + } + + Some(()) + } +} + +impl Msi { + pub fn parse<'a>(olecf: &Olecf<'a>) -> Result { + // Find the digital signature stream + let sig_stream = olecf.streams().find(|(name, _)| { + let clean = name.trim_start_matches(|c: char| c < '\u{20}'); + clean.eq_ignore_ascii_case("DigitalSignature") + }); + + let (sig_name, _) = + sig_stream.ok_or("No digital signature stream found")?; + let sig_data = olecf + .get_stream_data(sig_name) + .map_err(|_| "Failed to read digital signature stream")?; + + let hasher = MsiHasher { olecf }; + let signatures = AuthenticodeParser::parse(&sig_data, &hasher) + .map_err(|_| "Failed to parse signature")?; + + let mut msi_proto = MsiProto::new(); + let pb_signatures: Vec<_> = + signatures.iter().map(protos::pe::Signature::from).collect(); + + msi_proto.set_is_signed(!pb_signatures.is_empty()); + msi_proto.signatures = pb_signatures; + + Ok(msi_proto) + } +} diff --git a/lib/src/modules/msi/tests/testdata/28e9591338725a2a1b10ad3693f98fdfb3772acf88540da994628d73311d5593.in.zip b/lib/src/modules/msi/tests/testdata/28e9591338725a2a1b10ad3693f98fdfb3772acf88540da994628d73311d5593.in.zip new file mode 100644 index 000000000..1b77a662e Binary files /dev/null and b/lib/src/modules/msi/tests/testdata/28e9591338725a2a1b10ad3693f98fdfb3772acf88540da994628d73311d5593.in.zip differ diff --git a/lib/src/modules/msi/tests/testdata/28e9591338725a2a1b10ad3693f98fdfb3772acf88540da994628d73311d5593.out b/lib/src/modules/msi/tests/testdata/28e9591338725a2a1b10ad3693f98fdfb3772acf88540da994628d73311d5593.out new file mode 100644 index 000000000..70e73d61c --- /dev/null +++ b/lib/src/modules/msi/tests/testdata/28e9591338725a2a1b10ad3693f98fdfb3772acf88540da994628d73311d5593.out @@ -0,0 +1,66 @@ +is_signed: true +signatures: + - subject: "/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO Time Stamping Signer" + issuer: "/C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Object" + thumbprint: "3dbb6db5085c6dd5a1ca7f9cf84ecb1a3910cac8" + version: 3 + algorithm: "sha1WithRSAEncryption" + algorithm_oid: "1.2.840.113549.1.1.5" + serial: "47:8a:8e:fb:59:e1:d8:3f:0c:e1:42:d2:a2:87:07:be" + not_before: 1273449600 # 2010-05-10 00:00:00 UTC + not_after: 1431302399 # 2015-05-10 23:59:59 UTC + verified: false + digest_alg: "sha1" + digest: "046bddfc01a3a99d43f12d2eb398ca319de70bd7" + file_digest: "fd498f5737d19a6cd212dddc66876cb99bbe34a4" + number_of_certificates: 2 + number_of_countersignatures: 1 + signer_info: + program_name: "RAD PDF" + more_info: "http://www.radpdf.com " + digest: "32b81b8da4ad354b928ccc08fb5285fcbd83e199" + digest_alg: "sha1" + chain: + - issuer: "/C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Object" + subject: "/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO Time Stamping Signer" + thumbprint: "3dbb6db5085c6dd5a1ca7f9cf84ecb1a3910cac8" + version: 3 + algorithm: "sha1WithRSAEncryption" + algorithm_oid: "1.2.840.113549.1.1.5" + serial: "47:8a:8e:fb:59:e1:d8:3f:0c:e1:42:d2:a2:87:07:be" + not_before: 1273449600 # 2010-05-10 00:00:00 UTC + not_after: 1431302399 # 2015-05-10 23:59:59 UTC + certificates: + - issuer: "/C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Object" + subject: "/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO Time Stamping Signer" + thumbprint: "3dbb6db5085c6dd5a1ca7f9cf84ecb1a3910cac8" + version: 3 + algorithm: "sha1WithRSAEncryption" + algorithm_oid: "1.2.840.113549.1.1.5" + serial: "47:8a:8e:fb:59:e1:d8:3f:0c:e1:42:d2:a2:87:07:be" + not_before: 1273449600 # 2010-05-10 00:00:00 UTC + not_after: 1431302399 # 2015-05-10 23:59:59 UTC + - issuer: "/C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Object" + subject: "/C=US/postalCode=92024/ST=CA/L=Encinitas/O=Red Software/CN=Red Software" + thumbprint: "66d2ec68659e9dd90646258905b59353d5a5d259" + version: 3 + algorithm: "sha1WithRSAEncryption" + algorithm_oid: "1.2.840.113549.1.1.5" + serial: "1f:37:b6:d1:4d:22:11:b9:4b:d4:44:2d:da:ef:8a:1d" + not_before: 1294963200 # 2011-01-14 00:00:00 UTC + not_after: 1389657599 # 2014-01-13 23:59:59 UTC + countersignatures: + - verified: true + sign_time: 1381695422 # 2013-10-13 20:17:02 UTC + digest: "1e59b862158311a3fcfbf779dd908d157992ba2b" + digest_alg: "sha1" + chain: + - issuer: "/C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN-USERFirst-Object" + subject: "/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO Time Stamping Signer" + thumbprint: "3dbb6db5085c6dd5a1ca7f9cf84ecb1a3910cac8" + version: 3 + algorithm: "sha1WithRSAEncryption" + algorithm_oid: "1.2.840.113549.1.1.5" + serial: "47:8a:8e:fb:59:e1:d8:3f:0c:e1:42:d2:a2:87:07:be" + not_before: 1273449600 # 2010-05-10 00:00:00 UTC + not_after: 1431302399 # 2015-05-10 23:59:59 UTC \ No newline at end of file diff --git a/lib/src/modules/olecf/mod.rs b/lib/src/modules/olecf/mod.rs index 30dee1a4c..0d2153130 100644 --- a/lib/src/modules/olecf/mod.rs +++ b/lib/src/modules/olecf/mod.rs @@ -12,38 +12,45 @@ https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b use crate::errors::ModuleError; use crate::mods::prelude::*; use crate::modules::protos::olecf::*; +use crate::modules::utils::olecf::CachedOlecf; pub mod parser; #[cfg(test)] mod tests; -fn main(_ctx: &mut ModuleContext, data: &[u8]) -> Result { +fn main<'a>( + ctx: &mut ModuleContext<'a>, + data: &'a [u8], +) -> Result { + let cached = ctx.olecf_cache.get_or_insert_with(|| CachedOlecf::new(data)); let mut olecf = Olecf::new(); - match parser::OLECF::parse(data) { - Ok(parsed) => { - olecf.set_is_olecf(parsed.is_valid_header()); - olecf.streams = parsed - .streams() - .map(|(name, entry)| { - let mut s = Stream::new(); - s.set_name(name.to_string()); - s.set_size(entry.size); - s.set_type(match entry.stream_type { - parser::DirEntryType::Storage => StreamType::STORAGE, - parser::DirEntryType::Stream => StreamType::STREAM, - parser::DirEntryType::RootStorage => StreamType::ROOT, - _ => StreamType::UNKNOWN, - }); - s - }) - .collect(); - } - Err(_) => { + let cached = match cached { + CachedOlecf::Olecf(olecf) => olecf, + CachedOlecf::NotOlecf => { olecf.set_is_olecf(false); + return Ok(olecf); } - } + }; + + olecf.set_is_olecf(cached.is_valid_header()); + + olecf.streams = cached + .streams() + .map(|(name, entry)| { + let mut s = Stream::new(); + s.set_name(name.to_string()); + s.set_size(entry.size); + s.set_type(match entry.stream_type { + parser::DirEntryType::Storage => StreamType::STORAGE, + parser::DirEntryType::Stream => StreamType::STREAM, + parser::DirEntryType::RootStorage => StreamType::ROOT, + _ => StreamType::UNKNOWN, + }); + s + }) + .collect(); Ok(olecf) } diff --git a/lib/src/modules/olecf/parser.rs b/lib/src/modules/olecf/parser.rs index 178dc8bf4..a6b0dce4c 100644 --- a/lib/src/modules/olecf/parser.rs +++ b/lib/src/modules/olecf/parser.rs @@ -44,10 +44,10 @@ impl From for DirEntryType { /// A parser for OLE Compound File Binary Format (MS-CFB) files. /// -/// `OLECF` analyzes file headers, FAT/DIFAT allocation chains, directory +/// `Olecf` analyzes file headers, FAT/DIFAT allocation chains, directory /// entries, and stream contents for OLE compound documents (e.g., DOC, XLS, /// PPT, MSI). -pub struct OLECF<'a> { +pub struct Olecf<'a> { data: &'a [u8], sector_size: usize, mini_sector_size: usize, @@ -67,12 +67,12 @@ pub struct DirectoryEntry { pub stream_type: DirEntryType, } -impl<'a> OLECF<'a> { - /// Creates a new `OLECF` from a byte slice and initializes internal +impl<'a> Olecf<'a> { + /// Creates a new `Olecf` from a byte slice and initializes internal /// data structures by parsing the file header, FAT/DIFAT tables, and /// directory entries. pub fn parse(data: &'a [u8]) -> Result { - let mut olecf = OLECF { + let mut olecf = Olecf { data, sector_size: 0, mini_sector_size: 0, @@ -332,6 +332,16 @@ impl<'a> OLECF<'a> { Ok((_input, ())) } + /// Returns the underlying byte slice. + pub fn data(&self) -> &'a [u8] { + self.data + } + + /// Returns the sector size in bytes. + pub fn sector_size(&self) -> usize { + self.sector_size + } + /// Returns `true` if the underlying byte slice starts with a valid 8-byte /// OLECF signature (`0xD0CF11E0A1B11AE1`), or `false` otherwise. pub fn is_valid_header(&self) -> bool { @@ -526,13 +536,12 @@ impl<'a> OLECF<'a> { /// Core helper that extracts stream data by following a FAT or MiniFAT /// chain. /// - /// Attempts zero-copy slicing from `source_slice` if provided and if the - /// sector chain is strictly sequential. Falls back to allocating a vector - /// and reading sectors from `fallback_slice`. + /// Attempts zero-copy slicing from `stream_data` if it is a borrowed slice + /// and if the sector chain is strictly sequential. Falls back to allocating + /// a vector and reading sectors from `stream_data`. fn get_stream_data_by_chain( &self, - source_slice: Option<&'a [u8]>, - fallback_slice: &[u8], + stream_data: &Cow<'a, [u8]>, base_offset: u64, sector_size: usize, start_sector: u32, @@ -543,21 +552,22 @@ impl<'a> OLECF<'a> { return Ok(Cow::Borrowed(&[])); } - // Fast path: zero-copy slicing. - if let Some(src) = source_slice { - if let Ok(slice) = self.try_get_stream_slice( + // Fast path: zero-copy slicing if stream_data is borrowed. + if let Cow::Borrowed(src) = stream_data + && let Ok(slice) = self.try_get_stream_slice( src, base_offset, sector_size, start_sector, size, &next_sector_fn, - ) { - return Ok(Cow::Borrowed(slice)); - } + ) + { + return Ok(Cow::Borrowed(slice)); } // Fallback: Sector-by-sector gathering. + let fallback_slice = stream_data.as_ref(); let mut data = Vec::with_capacity(size); let mut current_sector = start_sector; let mut visited = Vec::new(); @@ -620,7 +630,7 @@ impl<'a> OLECF<'a> { return Err("Invalid start sector"); } - let needed_sectors = (size + sector_size - 1) / sector_size; + let needed_sectors = size.div_ceil(sector_size); let mut current_sector = start_sector; for _ in 1..needed_sectors { @@ -662,8 +672,7 @@ impl<'a> OLECF<'a> { return Err("Stream size exceeds maximum allowed size"); } self.get_stream_data_by_chain( - Some(self.data), - self.data, + &Cow::Borrowed(self.data), self.sector_size as u64, self.sector_size, start_sector, @@ -725,13 +734,8 @@ impl<'a> OLECF<'a> { } let mini_stream_data = self.get_root_mini_stream_data()?; - let source_slice = match &mini_stream_data { - Cow::Borrowed(slice) => Some(*slice), - Cow::Owned(_) => None, - }; self.get_stream_data_by_chain( - source_slice, &mini_stream_data, 0, self.mini_sector_size, diff --git a/lib/src/modules/olecf/tests/mod.rs b/lib/src/modules/olecf/tests/mod.rs index 24f0f9801..3832a7e06 100644 --- a/lib/src/modules/olecf/tests/mod.rs +++ b/lib/src/modules/olecf/tests/mod.rs @@ -1,6 +1,6 @@ -use std::borrow::Cow; +use crate::modules::olecf::parser::Olecf; use crate::modules::tests::create_binary_from_zipped_ihex; -use crate::modules::olecf::parser::OLECF; +use std::borrow::Cow; #[test] fn test_stream_data_extraction() { @@ -8,7 +8,7 @@ fn test_stream_data_extraction() { let data = create_binary_from_zipped_ihex( "src/modules/olecf/tests/testdata/reg_contiguous.in.zip", ); - let olecf = OLECF::parse(&data).unwrap(); + let olecf = Olecf::parse(&data).unwrap(); let stream = olecf.get_stream_data("ContiguousReg").unwrap(); assert!(matches!(stream, Cow::Borrowed(_))); assert_eq!(stream.len(), 5000); @@ -19,7 +19,7 @@ fn test_stream_data_extraction() { let data = create_binary_from_zipped_ihex( "src/modules/olecf/tests/testdata/reg_fragmented.in.zip", ); - let olecf = OLECF::parse(&data).unwrap(); + let olecf = Olecf::parse(&data).unwrap(); let stream = olecf.get_stream_data("FragReg").unwrap(); assert!(matches!(stream, Cow::Owned(_))); assert_eq!(stream.len(), 5000); @@ -30,7 +30,7 @@ fn test_stream_data_extraction() { let data = create_binary_from_zipped_ihex( "src/modules/olecf/tests/testdata/reg_cycle.in.zip", ); - let olecf = OLECF::parse(&data).unwrap(); + let olecf = Olecf::parse(&data).unwrap(); let err = olecf.get_stream_data("CycleReg").unwrap_err(); assert_eq!(err, "Circular reference detected in sector chain"); @@ -38,7 +38,7 @@ fn test_stream_data_extraction() { let data = create_binary_from_zipped_ihex( "src/modules/olecf/tests/testdata/mini_contiguous.in.zip", ); - let olecf = OLECF::parse(&data).unwrap(); + let olecf = Olecf::parse(&data).unwrap(); let stream = olecf.get_stream_data("ContiguousMini").unwrap(); assert!(matches!(stream, Cow::Borrowed(_))); assert_eq!(stream.len(), 120); @@ -49,7 +49,7 @@ fn test_stream_data_extraction() { let data = create_binary_from_zipped_ihex( "src/modules/olecf/tests/testdata/mini_fragmented.in.zip", ); - let olecf = OLECF::parse(&data).unwrap(); + let olecf = Olecf::parse(&data).unwrap(); let stream = olecf.get_stream_data("FragMini").unwrap(); assert!(matches!(stream, Cow::Owned(_))); assert_eq!(stream.len(), 120); @@ -60,7 +60,7 @@ fn test_stream_data_extraction() { let data = create_binary_from_zipped_ihex( "src/modules/olecf/tests/testdata/mini_in_frag_root.in.zip", ); - let olecf = OLECF::parse(&data).unwrap(); + let olecf = Olecf::parse(&data).unwrap(); let stream = olecf.get_stream_data("MiniInFragRoot").unwrap(); assert!(matches!(stream, Cow::Owned(_))); assert_eq!(stream.len(), 64); @@ -70,7 +70,7 @@ fn test_stream_data_extraction() { let data = create_binary_from_zipped_ihex( "src/modules/olecf/tests/testdata/mini_cycle.in.zip", ); - let olecf = OLECF::parse(&data).unwrap(); + let olecf = Olecf::parse(&data).unwrap(); let err = olecf.get_stream_data("CycleMini").unwrap_err(); assert_eq!(err, "Circular reference detected in sector chain"); @@ -78,7 +78,7 @@ fn test_stream_data_extraction() { let data = create_binary_from_zipped_ihex( "src/modules/olecf/tests/testdata/empty_stream.in.zip", ); - let olecf = OLECF::parse(&data).unwrap(); + let olecf = Olecf::parse(&data).unwrap(); let stream = olecf.get_stream_data("EmptyStream").unwrap(); assert!(matches!(stream, Cow::Borrowed(_))); assert!(stream.is_empty()); @@ -87,7 +87,7 @@ fn test_stream_data_extraction() { let data = create_binary_from_zipped_ihex( "src/modules/olecf/tests/testdata/incomplete_stream.in.zip", ); - let olecf = OLECF::parse(&data).unwrap(); + let olecf = Olecf::parse(&data).unwrap(); let err = olecf.get_stream_data("TruncatedStream").unwrap_err(); assert_eq!(err, "Incomplete stream data"); @@ -95,11 +95,10 @@ fn test_stream_data_extraction() { let data = create_binary_from_zipped_ihex( "src/modules/olecf/tests/testdata/v4_stream.in.zip", ); - let olecf = OLECF::parse(&data).unwrap(); + let olecf = Olecf::parse(&data).unwrap(); let stream = olecf.get_stream_data("V4DataStream").unwrap(); assert!(matches!(stream, Cow::Borrowed(_))); assert_eq!(stream.len(), 5000); assert_eq!(stream[0], 0xDD); assert_eq!(stream[4096], 0xEE); } - diff --git a/lib/src/modules/pe/mod.rs b/lib/src/modules/pe/mod.rs index a9385a54b..65872811d 100644 --- a/lib/src/modules/pe/mod.rs +++ b/lib/src/modules/pe/mod.rs @@ -24,7 +24,6 @@ use crate::types::Struct; #[cfg(test)] mod tests; -mod authenticode; pub mod parser; mod rva2off; diff --git a/lib/src/modules/pe/parser.rs b/lib/src/modules/pe/parser.rs index d858ab649..6b343f63a 100644 --- a/lib/src/modules/pe/parser.rs +++ b/lib/src/modules/pe/parser.rs @@ -24,11 +24,11 @@ use nom::number::complete::{le_u16, le_u32, le_u64, u8}; use nom::{Err, IResult, Parser, ToUsize}; use protobuf::{EnumOrUnknown, MessageField}; -use crate::modules::pe::authenticode::{ - AuthenticodeHasher, AuthenticodeParser, AuthenticodeSignature, -}; use crate::modules::pe::rva2off; use crate::modules::protos; +use crate::modules::utils::authenticode::{ + AuthenticodeHasher, AuthenticodeParser, AuthenticodeSignature, +}; type Error<'a> = nom::error::Error<&'a [u8]>; diff --git a/lib/src/modules/protos/generated/mod.rs b/lib/src/modules/protos/generated/mod.rs index 396b58d56..fa3e535a4 100644 --- a/lib/src/modules/protos/generated/mod.rs +++ b/lib/src/modules/protos/generated/mod.rs @@ -16,6 +16,7 @@ pub mod macho; pub mod magic; pub mod math; pub mod mods; +pub mod msi; pub mod net_analysis; pub mod olecf; pub mod pe; diff --git a/lib/src/modules/protos/generated/mods.rs b/lib/src/modules/protos/generated/mods.rs index 4caddb5d5..3273dd279 100644 --- a/lib/src/modules/protos/generated/mods.rs +++ b/lib/src/modules/protos/generated/mods.rs @@ -46,6 +46,8 @@ pub struct Modules { pub crx: ::protobuf::MessageField, // @@protoc_insertion_point(field:mods.Modules.dex) pub dex: ::protobuf::MessageField, + // @@protoc_insertion_point(field:mods.Modules.msi) + pub msi: ::protobuf::MessageField, // special fields // @@protoc_insertion_point(special_field:mods.Modules.special_fields) pub special_fields: ::protobuf::SpecialFields, @@ -63,7 +65,7 @@ impl Modules { } fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { - let mut fields = ::std::vec::Vec::with_capacity(9); + let mut fields = ::std::vec::Vec::with_capacity(10); let mut oneofs = ::std::vec::Vec::with_capacity(0); fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, super::pe::PE>( "pe", @@ -110,6 +112,11 @@ impl Modules { |m: &Modules| { &m.dex }, |m: &mut Modules| { &mut m.dex }, )); + fields.push(::protobuf::reflect::rt::v2::make_message_field_accessor::<_, super::msi::Msi>( + "msi", + |m: &Modules| { &m.msi }, + |m: &mut Modules| { &mut m.msi }, + )); ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( "Modules", fields, @@ -167,6 +174,11 @@ impl ::protobuf::Message for Modules { return false; } }; + for v in &self.msi { + if !v.is_initialized() { + return false; + } + }; true } @@ -200,6 +212,9 @@ impl ::protobuf::Message for Modules { 74 => { ::protobuf::rt::read_singular_message_into_field(is, &mut self.dex)?; }, + 82 => { + ::protobuf::rt::read_singular_message_into_field(is, &mut self.msi)?; + }, tag => { ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; }, @@ -248,6 +263,10 @@ impl ::protobuf::Message for Modules { let len = v.compute_size(); my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; } + if let Some(v) = self.msi.as_ref() { + let len = v.compute_size(); + my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; + } my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); self.special_fields.cached_size().set(my_size as u32); my_size @@ -281,6 +300,9 @@ impl ::protobuf::Message for Modules { if let Some(v) = self.dex.as_ref() { ::protobuf::rt::write_message_field_with_cached_size(9, v, os)?; } + if let Some(v) = self.msi.as_ref() { + ::protobuf::rt::write_message_field_with_cached_size(10, v, os)?; + } os.write_unknown_fields(self.special_fields.unknown_fields())?; ::std::result::Result::Ok(()) } @@ -307,6 +329,7 @@ impl ::protobuf::Message for Modules { self.vba.clear(); self.crx.clear(); self.dex.clear(); + self.msi.clear(); self.special_fields.clear(); } @@ -321,6 +344,7 @@ impl ::protobuf::Message for Modules { vba: ::protobuf::MessageField::none(), crx: ::protobuf::MessageField::none(), dex: ::protobuf::MessageField::none(), + msi: ::protobuf::MessageField::none(), special_fields: ::protobuf::SpecialFields::new(), }; &instance @@ -347,15 +371,16 @@ impl ::protobuf::reflect::ProtobufValue for Modules { static file_descriptor_proto_data: &'static [u8] = b"\ \n\nmods.proto\x12\x04mods\x1a\nyara.proto\x1a\tcrx.proto\x1a\tdex.proto\ \x1a\x0cdotnet.proto\x1a\telf.proto\x1a\x08pe.proto\x1a\tlnk.proto\x1a\ - \x0bmacho.proto\x1a\x0bolecf.proto\x1a\tvba.proto\"\x9d\x02\n\x07Modules\ - \x12\x16\n\x02pe\x18\x01\x20\x01(\x0b2\x06.pe.PER\x02pe\x12\x1a\n\x03elf\ - \x18\x02\x20\x01(\x0b2\x08.elf.ELFR\x03elf\x12&\n\x06dotnet\x18\x03\x20\ - \x01(\x0b2\x0e.dotnet.DotnetR\x06dotnet\x12\"\n\x05macho\x18\x04\x20\x01\ - (\x0b2\x0c.macho.MachoR\x05macho\x12\x1a\n\x03lnk\x18\x05\x20\x01(\x0b2\ - \x08.lnk.LnkR\x03lnk\x12\"\n\x05olecf\x18\x06\x20\x01(\x0b2\x0c.olecf.Ol\ - ecfR\x05olecf\x12\x1a\n\x03vba\x18\x07\x20\x01(\x0b2\x08.vba.VbaR\x03vba\ - \x12\x1a\n\x03crx\x18\x08\x20\x01(\x0b2\x08.crx.CrxR\x03crx\x12\x1a\n\ - \x03dex\x18\t\x20\x01(\x0b2\x08.dex.DexR\x03dexb\x06proto2\ + \x0bmacho.proto\x1a\x0bolecf.proto\x1a\tvba.proto\x1a\tmsi.proto\"\xb9\ + \x02\n\x07Modules\x12\x16\n\x02pe\x18\x01\x20\x01(\x0b2\x06.pe.PER\x02pe\ + \x12\x1a\n\x03elf\x18\x02\x20\x01(\x0b2\x08.elf.ELFR\x03elf\x12&\n\x06do\ + tnet\x18\x03\x20\x01(\x0b2\x0e.dotnet.DotnetR\x06dotnet\x12\"\n\x05macho\ + \x18\x04\x20\x01(\x0b2\x0c.macho.MachoR\x05macho\x12\x1a\n\x03lnk\x18\ + \x05\x20\x01(\x0b2\x08.lnk.LnkR\x03lnk\x12\"\n\x05olecf\x18\x06\x20\x01(\ + \x0b2\x0c.olecf.OlecfR\x05olecf\x12\x1a\n\x03vba\x18\x07\x20\x01(\x0b2\ + \x08.vba.VbaR\x03vba\x12\x1a\n\x03crx\x18\x08\x20\x01(\x0b2\x08.crx.CrxR\ + \x03crx\x12\x1a\n\x03dex\x18\t\x20\x01(\x0b2\x08.dex.DexR\x03dex\x12\x1a\ + \n\x03msi\x18\n\x20\x01(\x0b2\x08.msi.MsiR\x03msib\x06proto2\ "; /// `FileDescriptorProto` object which was a source for this generated file @@ -372,7 +397,7 @@ pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor { static file_descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::FileDescriptor> = ::protobuf::rt::Lazy::new(); file_descriptor.get(|| { let generated_file_descriptor = generated_file_descriptor_lazy.get(|| { - let mut deps = ::std::vec::Vec::with_capacity(10); + let mut deps = ::std::vec::Vec::with_capacity(11); deps.push(super::yara::file_descriptor().clone()); deps.push(super::crx::file_descriptor().clone()); deps.push(super::dex::file_descriptor().clone()); @@ -383,6 +408,7 @@ pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor { deps.push(super::macho::file_descriptor().clone()); deps.push(super::olecf::file_descriptor().clone()); deps.push(super::vba::file_descriptor().clone()); + deps.push(super::msi::file_descriptor().clone()); let mut messages = ::std::vec::Vec::with_capacity(1); messages.push(Modules::generated_message_descriptor_data()); let mut enums = ::std::vec::Vec::with_capacity(0); diff --git a/lib/src/modules/protos/generated/msi.rs b/lib/src/modules/protos/generated/msi.rs new file mode 100644 index 000000000..614a936b2 --- /dev/null +++ b/lib/src/modules/protos/generated/msi.rs @@ -0,0 +1,223 @@ +// This file is generated by rust-protobuf 3.7.2. Do not edit +// .proto file is parsed by pure +// @generated + +// https://github.com/rust-lang/rust-clippy/issues/702 +#![allow(unknown_lints)] +#![allow(clippy::all)] + +#![allow(unused_attributes)] +#![cfg_attr(rustfmt, rustfmt::skip)] + +#![allow(dead_code)] +#![allow(missing_docs)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(trivial_casts)] +#![allow(unused_results)] +#![allow(unused_mut)] + +//! Generated file from `msi.proto` + +/// Generated files are compatible only with the same version +/// of protobuf runtime. +const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_3_7_2; + +// @@protoc_insertion_point(message:msi.Msi) +#[derive(PartialEq,Clone,Default,Debug)] +pub struct Msi { + // message fields + // @@protoc_insertion_point(field:msi.Msi.is_signed) + pub is_signed: ::std::option::Option, + // @@protoc_insertion_point(field:msi.Msi.signatures) + pub signatures: ::std::vec::Vec, + // special fields + // @@protoc_insertion_point(special_field:msi.Msi.special_fields) + pub special_fields: ::protobuf::SpecialFields, +} + +impl<'a> ::std::default::Default for &'a Msi { + fn default() -> &'a Msi { + ::default_instance() + } +} + +impl Msi { + pub fn new() -> Msi { + ::std::default::Default::default() + } + + // optional bool is_signed = 1; + + pub fn is_signed(&self) -> bool { + self.is_signed.unwrap_or(false) + } + + pub fn clear_is_signed(&mut self) { + self.is_signed = ::std::option::Option::None; + } + + pub fn has_is_signed(&self) -> bool { + self.is_signed.is_some() + } + + // Param is passed by value, moved + pub fn set_is_signed(&mut self, v: bool) { + self.is_signed = ::std::option::Option::Some(v); + } + + fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { + let mut fields = ::std::vec::Vec::with_capacity(2); + let mut oneofs = ::std::vec::Vec::with_capacity(0); + fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( + "is_signed", + |m: &Msi| { &m.is_signed }, + |m: &mut Msi| { &mut m.is_signed }, + )); + fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( + "signatures", + |m: &Msi| { &m.signatures }, + |m: &mut Msi| { &mut m.signatures }, + )); + ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( + "Msi", + fields, + oneofs, + ) + } +} + +impl ::protobuf::Message for Msi { + const NAME: &'static str = "Msi"; + + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 8 => { + self.is_signed = ::std::option::Option::Some(is.read_bool()?); + }, + 18 => { + self.signatures.push(is.read_message()?); + }, + tag => { + ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u64 { + let mut my_size = 0; + if let Some(v) = self.is_signed { + my_size += 1 + 1; + } + for value in &self.signatures { + let len = value.compute_size(); + my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; + }; + my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); + self.special_fields.cached_size().set(my_size as u32); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + if let Some(v) = self.is_signed { + os.write_bool(1, v)?; + } + for v in &self.signatures { + ::protobuf::rt::write_message_field_with_cached_size(2, v, os)?; + }; + os.write_unknown_fields(self.special_fields.unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn special_fields(&self) -> &::protobuf::SpecialFields { + &self.special_fields + } + + fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { + &mut self.special_fields + } + + fn new() -> Msi { + Msi::new() + } + + fn clear(&mut self) { + self.is_signed = ::std::option::Option::None; + self.signatures.clear(); + self.special_fields.clear(); + } + + fn default_instance() -> &'static Msi { + static instance: Msi = Msi { + is_signed: ::std::option::Option::None, + signatures: ::std::vec::Vec::new(), + special_fields: ::protobuf::SpecialFields::new(), + }; + &instance + } +} + +impl ::protobuf::MessageFull for Msi { + fn descriptor() -> ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::Lazy::new(); + descriptor.get(|| file_descriptor().message_by_package_relative_name("Msi").unwrap()).clone() + } +} + +impl ::std::fmt::Display for Msi { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for Msi { + type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; +} + +static file_descriptor_proto_data: &'static [u8] = b"\ + \n\tmsi.proto\x12\x03msi\x1a\nyara.proto\x1a\x08pe.proto\"Q\n\x03Msi\x12\ + \x1b\n\tis_signed\x18\x01\x20\x01(\x08R\x08isSigned\x12-\n\nsignatures\ + \x18\x02\x20\x03(\x0b2\r.pe.SignatureR\nsignaturesB\x1e\xfa\x92\x19\x1a\ + \n\x03msi\x12\x07msi.Msi\x1a\nmsi-moduleb\x06proto2\ +"; + +/// `FileDescriptorProto` object which was a source for this generated file +fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto { + static file_descriptor_proto_lazy: ::protobuf::rt::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::Lazy::new(); + file_descriptor_proto_lazy.get(|| { + ::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap() + }) +} + +/// `FileDescriptor` object which allows dynamic access to files +pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor { + static generated_file_descriptor_lazy: ::protobuf::rt::Lazy<::protobuf::reflect::GeneratedFileDescriptor> = ::protobuf::rt::Lazy::new(); + static file_descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::FileDescriptor> = ::protobuf::rt::Lazy::new(); + file_descriptor.get(|| { + let generated_file_descriptor = generated_file_descriptor_lazy.get(|| { + let mut deps = ::std::vec::Vec::with_capacity(2); + deps.push(super::yara::file_descriptor().clone()); + deps.push(super::pe::file_descriptor().clone()); + let mut messages = ::std::vec::Vec::with_capacity(1); + messages.push(Msi::generated_message_descriptor_data()); + let mut enums = ::std::vec::Vec::with_capacity(0); + ::protobuf::reflect::GeneratedFileDescriptor::new_generated( + file_descriptor_proto(), + deps, + messages, + enums, + ) + }); + ::protobuf::reflect::FileDescriptor::new_generated_2(generated_file_descriptor) + }) +} diff --git a/lib/src/modules/protos/mods.proto b/lib/src/modules/protos/mods.proto index 52f6e77ea..12105901d 100644 --- a/lib/src/modules/protos/mods.proto +++ b/lib/src/modules/protos/mods.proto @@ -10,6 +10,7 @@ import "lnk.proto"; import "macho.proto"; import "olecf.proto"; import "vba.proto"; +import "msi.proto"; package mods; @@ -24,4 +25,5 @@ message Modules { optional vba.Vba vba = 7; optional crx.Crx crx = 8; optional dex.Dex dex = 9; + optional msi.Msi msi = 10; } diff --git a/lib/src/modules/protos/msi.proto b/lib/src/modules/protos/msi.proto new file mode 100644 index 000000000..1315165fe --- /dev/null +++ b/lib/src/modules/protos/msi.proto @@ -0,0 +1,19 @@ +syntax = "proto2"; + +import "yara.proto"; +import "pe.proto"; + +package msi; + +option (yara.module_options) = { + name : "msi" + root_message: "msi.Msi" + cargo_feature: "msi-module" +}; + +message Msi { + // True if the file is an MSI file and contains a digital signature. + optional bool is_signed = 1; + // Digital signatures present in the MSI file. + repeated pe.Signature signatures = 2; +} diff --git a/lib/src/modules/pe/authenticode.rs b/lib/src/modules/utils/authenticode.rs similarity index 95% rename from lib/src/modules/pe/authenticode.rs rename to lib/src/modules/utils/authenticode.rs index bf6e7497a..9ee8867c1 100644 --- a/lib/src/modules/pe/authenticode.rs +++ b/lib/src/modules/utils/authenticode.rs @@ -22,9 +22,9 @@ use log::error; use crate::modules::protos; use crate::modules::utils::asn1::{ - oid, oid_to_object_identifier, oid_to_str, Attribute, Certificate, - ContentInfo, DigestInfo, SignedData, SignerInfo, SpcIndirectDataContent, - SpcSpOpusInfo, TstInfo, + Attribute, Certificate, ContentInfo, DigestInfo, SignedData, SignerInfo, + SpcIndirectDataContent, SpcSpOpusInfo, TstInfo, oid, + oid_to_object_identifier, oid_to_str, }; use crate::modules::utils::crypto::PublicKey; @@ -46,7 +46,8 @@ pub enum ParseError { /// The number of digest algorithms is not 1. InvalidNumDigestAlgorithms(usize), - /// The encapsulated content type does not match [`SPC_INDIRECT_DATA_OBJID`]. + /// The encapsulated content type does not match + /// [`SPC_INDIRECT_DATA_OBJID`]. InvalidEncapsulatedContentType(String), /// The encapsulated content is not valid [`SpcIndirectDataContent`]. @@ -81,9 +82,9 @@ pub trait AuthenticodeHasher { /// Parses Authenticode signatures in a PE file. /// /// Some resources for understanding Authenticode signatures: -/// https://blog.trailofbits.com/2020/05/27/verifying-windows-binaries-without-windows/ -/// https://docs.clamav.net/appendix/Authenticode.html -/// https://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/authenticode_pe.docx +/// - +/// - +/// - pub struct AuthenticodeParser {} impl AuthenticodeParser { @@ -184,7 +185,7 @@ impl AuthenticodeParser { match signed_data.content_info.content.try_into() { Ok(idc) => idc, Err(_) => { - return Err(ParseError::InvalidSpcIndirectDataContent) + return Err(ParseError::InvalidSpcIndirectDataContent); } }; @@ -212,9 +213,10 @@ impl AuthenticodeParser { && let Ok(nested) = Self::parse_content_info( content_info, authenticode_hasher, - ) { - nested_signatures.extend(nested); - }; + ) + { + nested_signatures.extend(nested); + }; } } oid::MS_COUNTERSIGN => { @@ -523,17 +525,19 @@ impl<'a> AuthenticodeSignature<'a> { /// Returns `true` if the [`AuthenticodeSignature`] is valid. /// - /// A valid Authenticode signature must comply with the following requisites: + /// A valid Authenticode signature must comply with the following + /// requisites: /// /// * The Authenticode hash included in the file (in the `message_digest` /// field of [`SpcIndirectDataContent`]) must match the hash computed by /// ourselves using [`PE::authenticode_hash`]. This ensures that the file /// has not been modified. /// - /// * The message digest stored the signed attribute [`rfc6268::ID_MESSAGE_DIGEST`] - /// of [`SignerInfo`], must match the one computed by ourselves by hashing - /// the `econtent` field in [`EncapsulatedContentInfo`]. This ensures that - /// the Authenticode hash included in the file has not been tampered. + /// * The message digest stored the signed attribute + /// [`rfc6268::ID_MESSAGE_DIGEST`] of [`SignerInfo`], must match the one + /// computed by ourselves by hashing the `econtent` field in + /// [`EncapsulatedContentInfo`]. This ensures that the Authenticode hash + /// included in the file has not been tampered. /// /// * The signature in [`SignerInfo`] must be valid. This signature is the /// result of signing the hash of the DER encoding of the signed @@ -610,17 +614,18 @@ impl From<&AuthenticodeSignature<'_>> for protos::pe::Signature { // `chain` field in `SignerInfo` didn't exist in previous versions of // YARA. if let Some(signer_info) = sig.signer_info.as_ref() - && let Some(cert) = signer_info.chain.first() { - sig.version = cert.version; - sig.thumbprint.clone_from(&cert.thumbprint); - sig.issuer.clone_from(&cert.issuer); - sig.subject.clone_from(&cert.subject); - sig.serial.clone_from(&cert.serial); - sig.not_after = cert.not_after; - sig.not_before = cert.not_before; - sig.algorithm.clone_from(&cert.algorithm); - sig.algorithm_oid.clone_from(&cert.algorithm_oid); - } + && let Some(cert) = signer_info.chain.first() + { + sig.version = cert.version; + sig.thumbprint.clone_from(&cert.thumbprint); + sig.issuer.clone_from(&cert.issuer); + sig.subject.clone_from(&cert.subject); + sig.serial.clone_from(&cert.serial); + sig.not_after = cert.not_after; + sig.not_before = cert.not_before; + sig.algorithm.clone_from(&cert.algorithm); + sig.algorithm_oid.clone_from(&cert.algorithm_oid); + } sig } @@ -678,14 +683,14 @@ impl From<&Certificate<'_>> for protos::pe::Certificate { /// resulting string follows the [RFC 4514], resulting in something like: /// /// ```text -/// CN=Thawte Timestamping CA,OU=Thawte Certification,O=Thawte,L=Durbanville,ST=Western Cape,C=ZA +/// CN=Thawte Timestamping CA,OU=Thawte Certification,O=Thawte, L=Durbanville,ST=Western Cape,C=ZA /// ``` /// /// However, the format traditionally used by YARA is inherited from OpenSSL /// and looks like: /// /// ```text -/// /C=ZA/ST=Western Cape/L=Durbanville/O=Thawte/OU=Thawte Certification/CN=Thawte Timestamping CA +/// /C=ZA/ST=Western Cape/L=Durbanville/O=Thawte/OU=Thawte Certification/ CN=Thawte Timestamping CA /// ``` /// /// [RFC 4514]: https://datatracker.ietf.org/doc/html/rfc4514 @@ -1006,13 +1011,15 @@ impl<'a, 'b> Iterator for CertificateChain<'a, 'b> { self.next = self .certs .iter() - // The next certificate must be the issuer of the current one... + // The next certificate must be the issuer of the + // current one... .find(|c| { c.x509.tbs_certificate.subject == next.x509.tbs_certificate.issuer }) - // ... except if the issuer was already returned by the iterator, - // which indicates that the certificate chain contains a loop. + // ... except if the issuer was already returned by the + // iterator, which indicates that the certificate chain + // contains a loop. .filter(|c| { self.seen .insert(c.x509.tbs_certificate.subject.as_raw()) diff --git a/lib/src/modules/utils/mod.rs b/lib/src/modules/utils/mod.rs index 1170257fb..d3cd14384 100644 --- a/lib/src/modules/utils/mod.rs +++ b/lib/src/modules/utils/mod.rs @@ -1,10 +1,19 @@ #[cfg(feature = "crypto")] pub mod asn1; +#[cfg(feature = "crypto")] +pub mod authenticode; + #[cfg(feature = "crypto")] pub mod crypto; #[cfg(feature = "crypto")] pub mod leb128; +#[cfg(any( + feature = "olecf-module", + feature = "msi-module", + feature = "vba-module" +))] +pub mod olecf; #[cfg(any(feature = "zip-module", feature = "vba-module"))] pub mod zip; diff --git a/lib/src/modules/utils/olecf.rs b/lib/src/modules/utils/olecf.rs new file mode 100644 index 000000000..5d086d768 --- /dev/null +++ b/lib/src/modules/utils/olecf.rs @@ -0,0 +1,15 @@ +use crate::modules::olecf::parser::Olecf; + +pub(crate) enum CachedOlecf<'a> { + NotOlecf, + Olecf(Olecf<'a>), +} + +impl<'a> CachedOlecf<'a> { + pub(crate) fn new(data: &'a [u8]) -> Self { + match Olecf::parse(data) { + Ok(olecf) => CachedOlecf::Olecf(olecf), + Err(_) => CachedOlecf::NotOlecf, + } + } +} diff --git a/lib/src/modules/utils/zip.rs b/lib/src/modules/utils/zip.rs index 95f94b7f0..f887dc4a2 100644 --- a/lib/src/modules/utils/zip.rs +++ b/lib/src/modules/utils/zip.rs @@ -5,27 +5,27 @@ use protobuf::Enum; use rustc_hash::FxHashMap; use tinyzip::Archive; -use crate::modules::protos::zip::{Compression, Entry, Zip}; +use crate::modules::protos::zip::{Compression, Entry, Zip as ZipProto}; -pub(crate) enum ZipCache<'a> { +pub(crate) enum CachedZip<'a> { NotZip, - Cached(CachedZip<'a>), + Zip(Zip<'a>), } -pub(crate) struct CachedZip<'a> { +pub(crate) struct Zip<'a> { pub data: &'a [u8], pub archive: Archive<&'a [u8]>, pub cached_contents: FxHashMap>, } -impl<'a> ZipCache<'a> { +impl<'a> CachedZip<'a> { pub(crate) fn new(data: &'a [u8]) -> Self { let archive = match Archive::open(data) { Ok(arch) => arch, - Err(_) => return ZipCache::NotZip, + Err(_) => return CachedZip::NotZip, }; - ZipCache::Cached(CachedZip { + CachedZip::Zip(Zip { data, archive, cached_contents: FxHashMap::default(), @@ -33,11 +33,13 @@ impl<'a> ZipCache<'a> { } } -impl<'a> CachedZip<'a> { +impl<'a> Zip<'a> { pub(crate) fn get_file_content<'b>( &'b mut self, path: &str, ) -> Option<&'b [u8]> { + // Check if the content for the given path is already cached, it not, + // put it into the cache. if !self.cached_contents.contains_key(path) { let entry = self.archive.find_file(path).ok()?; let data_range = entry.data_range().ok()?.data_range; @@ -67,13 +69,15 @@ impl<'a> CachedZip<'a> { self.cached_contents.insert(path.to_string(), content); } + // At this point the content for the given path must be already in the + // cache. Some(self.cached_contents.get(path).unwrap().as_ref()) } } -impl<'a> From<&CachedZip<'a>> for Zip { - fn from(cached: &CachedZip<'a>) -> Self { - let mut zip = Zip::new(); +impl<'a> From<&Zip<'a>> for ZipProto { + fn from(cached: &Zip<'a>) -> Self { + let mut zip = ZipProto::new(); zip.set_is_zip(true); let mut entries = Vec::new(); @@ -124,17 +128,17 @@ mod tests { #[test] fn test_zip_cache() { assert!(matches!( - ZipCache::new(b"invalid zip data"), - ZipCache::NotZip + CachedZip::new(b"invalid zip data"), + CachedZip::NotZip )); let eocd = [ 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; - if let ZipCache::Cached(mut cached) = ZipCache::new(&eocd) { + if let CachedZip::Zip(mut cached) = CachedZip::new(&eocd) { assert!(cached.get_file_content("missing.txt").is_none()); - let zip_proto: Zip = (&cached).into(); + let zip_proto: ZipProto = (&cached).into(); assert!(zip_proto.is_zip()); assert_eq!(zip_proto.entries.len(), 0); } diff --git a/lib/src/modules/vba/mod.rs b/lib/src/modules/vba/mod.rs index edaaa26a9..07908bbce 100644 --- a/lib/src/modules/vba/mod.rs +++ b/lib/src/modules/vba/mod.rs @@ -10,24 +10,18 @@ use rustc_hash::FxHashMap as HashMap; use std::borrow::Cow; use crate::mods::prelude::*; -use crate::modules::olecf::parser::OLECF; +use crate::modules::olecf::parser::Olecf; use crate::modules::protos::vba::*; -use crate::modules::utils::zip::ZipCache; +use crate::modules::utils::olecf::CachedOlecf; +use crate::modules::utils::zip::{CachedZip, Zip}; mod parser; -#[derive(Debug)] -struct VbaExtractor<'a> { - data: &'a [u8], -} +struct VbaExtractor; -impl<'a> VbaExtractor<'a> { - fn new(data: &'a [u8]) -> Self { - Self { data } - } - - fn read_stream_data( - olecf: &OLECF<'a>, +impl VbaExtractor { + fn read_stream_data<'a>( + olecf: &Olecf<'a>, name: &str, ) -> Result, &'static str> { let size = olecf.get_stream_size(name)? as usize; @@ -37,9 +31,8 @@ impl<'a> VbaExtractor<'a> { olecf.get_stream_data(name) } - fn extract_from_ole_bytes(ole_data: &'a [u8]) -> Result { - let olecf = OLECF::parse(ole_data)?; - let stream_names = olecf.get_stream_names()?; + fn extract_from_olecf<'a>(olecf: &Olecf<'a>) -> Result { + let stream_names = olecf.stream_names()?; let mut vba_dir = None; let mut modules = HashMap::default(); @@ -47,7 +40,7 @@ impl<'a> VbaExtractor<'a> { // First process the dir stream if let Some(dir_name) = stream_names.iter().find(|n| n.trim().eq_ignore_ascii_case("dir")) - && let Ok(data) = Self::read_stream_data(&olecf, dir_name) + && let Ok(data) = Self::read_stream_data(olecf, dir_name) { vba_dir = Some(data); } @@ -55,7 +48,7 @@ impl<'a> VbaExtractor<'a> { // Then process other streams for name in &stream_names { if !name.trim().eq_ignore_ascii_case("dir") - && let Ok(data) = Self::read_stream_data(&olecf, name) + && let Ok(data) = Self::read_stream_data(olecf, name) && !data.is_empty() { let lowercase_name = name.to_lowercase(); @@ -73,35 +66,23 @@ impl<'a> VbaExtractor<'a> { } } - fn extract_from_ole(&self) -> Result { - Self::extract_from_ole_bytes(self.data) - } -} - -fn extract_from_zip<'a>( - ctx: &mut ModuleContext<'a>, - data: &'a [u8], -) -> Result { - let zip_cache = ctx.zip_cache.get_or_insert_with(|| ZipCache::new(data)); - - let ZipCache::Cached(cached_zip) = zip_cache else { - return Err("no VBA project found in ZIP"); - }; - - let vba_project_names = [ - "word/vbaProject.bin", - "xl/vbaProject.bin", - "ppt/vbaProject.bin", - "vbaProject.bin", - ]; - - for name in &vba_project_names { - if let Some(contents) = cached_zip.get_file_content(name) { - return VbaExtractor::extract_from_ole_bytes(contents); + fn extract_from_zip<'a>(zip: &mut Zip<'a>) -> Result { + let vba_project_names = [ + "word/vbaProject.bin", + "xl/vbaProject.bin", + "ppt/vbaProject.bin", + "vbaProject.bin", + ]; + + for name in &vba_project_names { + if let Some(contents) = zip.get_file_content(name) { + let olecf = Olecf::parse(contents)?; + return Self::extract_from_olecf(&olecf); + } } - } - Err("no VBA project found in ZIP") + Err("no VBA project found in ZIP") + } } fn main<'a>( @@ -111,9 +92,15 @@ fn main<'a>( let is_zip = data.starts_with(&[0x50, 0x4B, 0x03, 0x04]); let project = if is_zip { - extract_from_zip(ctx, data) + match ctx.zip_cache.get_or_insert_with(|| CachedZip::new(data)) { + CachedZip::Zip(z) => VbaExtractor::extract_from_zip(z), + CachedZip::NotZip => Err("not a valid ZIP archive"), + } } else { - VbaExtractor::new(data).extract_from_ole() + match ctx.olecf_cache.get_or_insert_with(|| CachedOlecf::new(data)) { + CachedOlecf::Olecf(o) => VbaExtractor::extract_from_olecf(o), + CachedOlecf::NotOlecf => Err("not a valid OLECF file"), + } }; let vba = match project { diff --git a/lib/src/modules/zip/mod.rs b/lib/src/modules/zip/mod.rs index cc681b860..14dfdec07 100644 --- a/lib/src/modules/zip/mod.rs +++ b/lib/src/modules/zip/mod.rs @@ -3,16 +3,16 @@ use std::ops::Deref; use crate::mods::prelude::*; use crate::modules::ModuleError; use crate::modules::protos::zip::Zip; -use crate::modules::utils::zip::ZipCache; +use crate::modules::utils::zip::CachedZip; use crate::register_module; pub fn main<'a>( ctx: &mut ModuleContext<'a>, data: &'a [u8], ) -> Result { - match ctx.zip_cache.get_or_insert_with(|| ZipCache::new(data)) { - ZipCache::Cached(zip) => Ok(zip.deref().into()), - ZipCache::NotZip => { + match ctx.zip_cache.get_or_insert_with(|| CachedZip::new(data)) { + CachedZip::Zip(zip) => Ok(zip.deref().into()), + CachedZip::NotZip => { let mut zip = Zip::new(); zip.set_is_zip(false); Ok(zip) diff --git a/lib/src/wasm/string.rs b/lib/src/wasm/string.rs index ee6313715..323767bc9 100644 --- a/lib/src/wasm/string.rs +++ b/lib/src/wasm/string.rs @@ -27,25 +27,30 @@ pub trait String: Default { /// [`RuntimeString`] to and from WASM, it must be represented as one of those /// primitive types. /// -/// The `u64` value contains all the information required for uniquely +/// The `i64` value contains all the information required for uniquely /// identifying the string. This is how the information is encoded: /// -/// * `RuntimeString:Undef` -> `0` -/// A zero represents an undefined string. -/// -/// * `RuntimeString:Literal` -> `LiteralId << 2 | 1` -/// If the two lower bits are equal to 1, it's a literal string, where the +/// * `RuntimeString:Literal` -> `LiteralId << 2 | 0` +/// If the two lower bits are equal to 0, it's a literal string, where the /// remaining bits represent the `LiteralId`. /// -/// * `RuntimeString:Rc` -> `RuntimeStringId << 2 | 2` -/// If the two lower bits are equal to 2, it's a runtime string, where the -/// remaining bits represent the handle of a string object. +/// * `RuntimeString:Rc` -> `RuntimeObjectHandle | 1` +/// If the two lower bits are equal to 1, it's a reference-counted string, +/// where the handle is the pointer to the string object (which is aligned to +/// at least 4 bytes, so its 2 lower bits are 0). /// -/// * `RuntimeString:ScannedDataSlice` -> `Offset << 18 | Len << 2 | 3)` -/// If the two lower bits are 3, it's a string backed by the scanned data. -/// Bits 18:3 ar used for representing the string length (up to 64KB), -/// while bits 64:19 represent the offset (up to 70,368,744,177,663). +/// * `RuntimeString:ScannedDataSlice` -> `Offset << 18 | Len << 2 | 2` +/// If the two lower bits are 2, it's a string backed by the scanned data. +/// Bits 17:2 are used for representing the string length (up to 64KB), +/// while bits 63:18 represent the offset (up to 70,368,744,177,663). /// +/// Tags are stored in the lower 2 bits rather than the higher bits because +/// heap pointers (`RuntimeObjectHandle`) in Rust are aligned to at least +/// 4-byte boundaries (meaning their two lower bits are guaranteed to be `00`). +/// Storing the tag in the lower bits via bitwise OR (`handle | 1`) preserves +/// all 64 bits of pointer addresses without bit-shifting. This avoids bit +/// truncation or sign-extension overflow issues across high 64-bit memory +/// address spaces (such as ASLR). pub(crate) type RuntimeStringWasm = i64; /// String types handled by YARA's WASM runtime. @@ -106,7 +111,7 @@ impl String for RuntimeString { Self::Literal(id) => i64::from(id) << 2, Self::Rc(s) => { let handle: i64 = ctx.store_string(s).into(); - (handle << 2) | 1 + handle | 1 } Self::ScannedDataSlice { offset, length } => { if length >= u16::MAX as usize { @@ -191,7 +196,7 @@ impl RuntimeString { match s & 0x3 { 0 => Self::Literal(LiteralId::from((s >> 2) as u32)), 1 => { - let handle = RuntimeObjectHandle::from(s >> 2); + let handle = RuntimeObjectHandle::from(s & !3); let s = cast!( ctx.runtime_objects.get(&handle).unwrap(), RuntimeObject::String diff --git a/ls/src/tests/testdata/completion10.response.json b/ls/src/tests/testdata/completion10.response.json index b10a8786c..5e4089845 100644 --- a/ls/src/tests/testdata/completion10.response.json +++ b/ls/src/tests/testdata/completion10.response.json @@ -49,6 +49,16 @@ "label": "math", "preselect": true }, + { + "kind": 9, + "label": "msi", + "preselect": true + }, + { + "kind": 9, + "label": "olecf", + "preselect": true + }, { "kind": 9, "label": "pe", @@ -74,6 +84,11 @@ "label": "time", "preselect": true }, + { + "kind": 9, + "label": "vba", + "preselect": true + }, { "kind": 9, "label": "vt", diff --git a/ls/src/tests/testdata/completion11.response.json b/ls/src/tests/testdata/completion11.response.json index b10a8786c..5e4089845 100644 --- a/ls/src/tests/testdata/completion11.response.json +++ b/ls/src/tests/testdata/completion11.response.json @@ -49,6 +49,16 @@ "label": "math", "preselect": true }, + { + "kind": 9, + "label": "msi", + "preselect": true + }, + { + "kind": 9, + "label": "olecf", + "preselect": true + }, { "kind": 9, "label": "pe", @@ -74,6 +84,11 @@ "label": "time", "preselect": true }, + { + "kind": 9, + "label": "vba", + "preselect": true + }, { "kind": 9, "label": "vt", diff --git a/ls/src/tests/testdata/completion14.response.json b/ls/src/tests/testdata/completion14.response.json index dc386473a..3e0a82683 100644 --- a/ls/src/tests/testdata/completion14.response.json +++ b/ls/src/tests/testdata/completion14.response.json @@ -296,6 +296,44 @@ "kind": 9, "label": "math" }, + { + "additionalTextEdits": [ + { + "newText": "import \"msi\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "msi" + }, + { + "additionalTextEdits": [ + { + "newText": "import \"olecf\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "olecf" + }, { "kind": 9, "label": "pe" @@ -376,6 +414,25 @@ "kind": 9, "label": "time" }, + { + "additionalTextEdits": [ + { + "newText": "import \"vba\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "vba" + }, { "additionalTextEdits": [ { diff --git a/ls/src/tests/testdata/completion15.response.json b/ls/src/tests/testdata/completion15.response.json index 786644415..9417940cf 100644 --- a/ls/src/tests/testdata/completion15.response.json +++ b/ls/src/tests/testdata/completion15.response.json @@ -275,6 +275,44 @@ "kind": 9, "label": "math" }, + { + "additionalTextEdits": [ + { + "newText": "import \"msi\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "msi" + }, + { + "additionalTextEdits": [ + { + "newText": "import \"olecf\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "olecf" + }, { "kind": 9, "label": "pe" @@ -355,6 +393,25 @@ "kind": 9, "label": "time" }, + { + "additionalTextEdits": [ + { + "newText": "import \"vba\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "vba" + }, { "additionalTextEdits": [ { diff --git a/ls/src/tests/testdata/completion2.response.json b/ls/src/tests/testdata/completion2.response.json index 4503a49c4..9a18c11a9 100644 --- a/ls/src/tests/testdata/completion2.response.json +++ b/ls/src/tests/testdata/completion2.response.json @@ -282,6 +282,44 @@ "kind": 9, "label": "math" }, + { + "additionalTextEdits": [ + { + "newText": "import \"msi\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "msi" + }, + { + "additionalTextEdits": [ + { + "newText": "import \"olecf\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "olecf" + }, { "additionalTextEdits": [ { @@ -377,6 +415,25 @@ "kind": 9, "label": "time" }, + { + "additionalTextEdits": [ + { + "newText": "import \"vba\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "vba" + }, { "additionalTextEdits": [ { diff --git a/ls/src/tests/testdata/completion23.response.json b/ls/src/tests/testdata/completion23.response.json index cf1d47fd3..d603b2399 100644 --- a/ls/src/tests/testdata/completion23.response.json +++ b/ls/src/tests/testdata/completion23.response.json @@ -275,6 +275,44 @@ "kind": 9, "label": "math" }, + { + "additionalTextEdits": [ + { + "newText": "import \"msi\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "msi" + }, + { + "additionalTextEdits": [ + { + "newText": "import \"olecf\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "olecf" + }, { "additionalTextEdits": [ { @@ -370,6 +408,25 @@ "kind": 9, "label": "time" }, + { + "additionalTextEdits": [ + { + "newText": "import \"vba\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "vba" + }, { "additionalTextEdits": [ { diff --git a/ls/src/tests/testdata/completion24.response.json b/ls/src/tests/testdata/completion24.response.json index 9c25cd5a6..1603699d6 100644 --- a/ls/src/tests/testdata/completion24.response.json +++ b/ls/src/tests/testdata/completion24.response.json @@ -287,6 +287,44 @@ "kind": 9, "label": "math" }, + { + "additionalTextEdits": [ + { + "newText": "import \"msi\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "msi" + }, + { + "additionalTextEdits": [ + { + "newText": "import \"olecf\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "olecf" + }, { "additionalTextEdits": [ { @@ -382,6 +420,25 @@ "kind": 9, "label": "time" }, + { + "additionalTextEdits": [ + { + "newText": "import \"vba\"\n", + "range": { + "end": { + "character": 0, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + } + } + ], + "kind": 9, + "label": "vba" + }, { "additionalTextEdits": [ {