diff options
author | Omer Ben-Amram <omerbenamram@gmail.com> | 2019-05-29 23:40:46 +0300 |
---|---|---|
committer | Omer Ben-Amram <omerbenamram@gmail.com> | 2019-05-29 23:40:46 +0300 |
commit | 93c466bbb54211368881f5dbff9ad88bb286ecf3 (patch) | |
tree | 3ea2d4f854539c5cc63f4f6981874b8021534405 | |
parent | 455980680f4793bbc8aab8e146a456073da9e984 (diff) |
fixed attributes with base reference
-rw-r--r-- | src/attribute/header.rs | 16 | ||||
-rw-r--r-- | src/attribute/mod.rs | 15 | ||||
-rw-r--r-- | src/attribute/x20.rs | 64 | ||||
-rw-r--r-- | src/entry.rs | 28 | ||||
-rw-r--r-- | src/mft.rs | 76 |
5 files changed, 161 insertions, 38 deletions
diff --git a/src/attribute/header.rs b/src/attribute/header.rs index 3d3121d..df19697 100644 --- a/src/attribute/header.rs +++ b/src/attribute/header.rs @@ -6,7 +6,7 @@ use crate::ReadSeek; use byteorder::{LittleEndian, ReadBytesExt}; use num_traits::FromPrimitive; use serde::Serialize; -use std::io::Read; +use std::io::{Read, SeekFrom}; /// Represents the union defined in /// https://docs.microsoft.com/en-us/windows/desktop/devnotes/attribute-record-header @@ -43,6 +43,8 @@ impl MftAttributeHeader { /// Tries to read an AttributeHeader from the stream. /// Will return `None` if the type code is $END. pub fn from_stream<S: ReadSeek>(stream: &mut S) -> Result<Option<MftAttributeHeader>> { + let attribute_header_start_offset = stream.tell()?; + let type_code_value = stream.read_u32::<LittleEndian>()?; if type_code_value == 0xFFFF_FFFF { @@ -76,8 +78,8 @@ impl MftAttributeHeader { let id = stream.read_u16::<LittleEndian>()?; let residential_header = match resident_flag { - 0 => ResidentialHeader::Resident(ResidentHeader::new(stream)?), - 1 => ResidentialHeader::NonResident(NonResidentHeader::new(stream)?), + 0 => ResidentialHeader::Resident(ResidentHeader::from_stream(stream)?), + 1 => ResidentialHeader::NonResident(NonResidentHeader::from_stream(stream)?), _ => { return err::UnhandledResidentFlag { flag: resident_flag, @@ -89,6 +91,10 @@ impl MftAttributeHeader { // Name is optional, and will not be present if size == 0. let name = if name_size > 0 { + stream.seek(SeekFrom::Start( + attribute_header_start_offset + + u64::from(name_offset.expect("name_size > 0 is invariant")), + ))?; read_utf16_string(stream, Some(name_size as usize))? } else { String::new() @@ -122,7 +128,7 @@ pub struct ResidentHeader { } impl ResidentHeader { - pub fn new<R: Read>(reader: &mut R) -> Result<ResidentHeader> { + pub fn from_stream<R: Read>(reader: &mut R) -> Result<ResidentHeader> { Ok(ResidentHeader { data_size: reader.read_u32::<LittleEndian>()?, data_offset: reader.read_u16::<LittleEndian>()?, @@ -158,7 +164,7 @@ pub struct NonResidentHeader { } impl NonResidentHeader { - pub fn new<R: Read>(reader: &mut R) -> Result<NonResidentHeader> { + pub fn from_stream<R: Read>(reader: &mut R) -> Result<NonResidentHeader> { let vnc_first = reader.read_u64::<LittleEndian>()?; let vnc_last = reader.read_u64::<LittleEndian>()?; let datarun_offset = reader.read_u16::<LittleEndian>()?; diff --git a/src/attribute/mod.rs b/src/attribute/mod.rs index 8b3acde..c397a2e 100644 --- a/src/attribute/mod.rs +++ b/src/attribute/mod.rs @@ -1,6 +1,7 @@ pub mod header; pub mod raw; pub mod x10; +pub mod x20; pub mod x30; pub mod x40; pub mod x80; @@ -13,6 +14,7 @@ use bitflags::bitflags; use crate::attribute::raw::RawAttribute; use crate::attribute::x10::StandardInfoAttr; +use crate::attribute::x20::AttributeListAttr; use crate::attribute::x30::FileNameAttr; use crate::attribute::header::{MftAttributeHeader, ResidentHeader}; @@ -37,6 +39,9 @@ impl MftAttributeContent { MftAttributeType::StandardInformation => Ok(MftAttributeContent::AttrX10( StandardInfoAttr::from_reader(stream)?, )), + MftAttributeType::AttributeList => Ok(MftAttributeContent::AttrX20( + AttributeListAttr::from_stream(stream)?, + )), MftAttributeType::FileName => Ok(MftAttributeContent::AttrX30( FileNameAttr::from_stream(stream)?, )), @@ -61,6 +66,13 @@ impl MftAttributeContent { )?)), } } + + pub fn into_file_name(self) -> Option<FileNameAttr> { + match self { + MftAttributeContent::AttrX30(content) => Some(content), + _ => None, + } + } } #[derive(Serialize, Clone, Debug)] @@ -69,6 +81,7 @@ pub enum MftAttributeContent { Raw(RawAttribute), AttrX80(DataAttr), AttrX10(StandardInfoAttr), + AttrX20(AttributeListAttr), AttrX30(FileNameAttr), AttrX40(ObjectIdAttr), AttrX90(IndexRootAttr), @@ -88,6 +101,8 @@ pub enum MftAttributeType { FileName = 0x30_u32, /// An 16-byte object identifier assigned by the link-tracking service. ObjectId = 0x40_u32, + /// File's access control list and security properties + SecurityDescriptor = 0x50_u32, /// The volume label. /// Present in the $Volume file. VolumeName = 0x60_u32, diff --git a/src/attribute/x20.rs b/src/attribute/x20.rs new file mode 100644 index 0000000..1309de4 --- /dev/null +++ b/src/attribute/x20.rs @@ -0,0 +1,64 @@ +use crate::err::{self, Result}; +use crate::ReadSeek; +use log::trace; +use snafu::OptionExt; + +use byteorder::{LittleEndian, ReadBytesExt}; +use encoding::all::UTF_16LE; +use encoding::{DecoderTrap, Encoding}; + +use serde::Serialize; + +use snafu::ResultExt; +use std::io::SeekFrom; +use winstructs::ntfs::mft_reference::MftReference; + +#[derive(Serialize, Clone, Debug)] +pub struct AttributeListAttr { + pub attribute_type: u32, + pub record_length: u16, + pub first_vcn: u64, + pub base_reference: MftReference, + pub attribute_id: u16, + pub name: String, +} + +impl AttributeListAttr { + pub fn from_stream<S: ReadSeek>(stream: &mut S) -> Result<AttributeListAttr> { + let start_offset = stream.tell()?; + + trace!("Offset {}: AttributeListAttr", start_offset); + + let attribute_type = stream.read_u32::<LittleEndian>()?; + let record_length = stream.read_u16::<LittleEndian>()?; + let name_length = stream.read_u8()?; + let name_offset = stream.read_u8()?; + let first_vcn = stream.read_u64::<LittleEndian>()?; + let base_reference = + MftReference::from_reader(stream).context(err::FailedToReadMftReference)?; + let attribute_id = stream.read_u16::<LittleEndian>()?; + + let name = if name_length > 0 { + stream.seek(SeekFrom::Start(start_offset + u64::from(name_offset)))?; + + let mut name_buffer = vec![0; (name_length as usize * 2) as usize]; + stream.read_exact(&mut name_buffer)?; + + match UTF_16LE.decode(&name_buffer, DecoderTrap::Ignore) { + Ok(s) => s, + Err(_e) => return err::InvalidFilename {}.fail(), + } + } else { + String::new() + }; + + Ok(AttributeListAttr { + attribute_type, + record_length, + first_vcn, + base_reference, + attribute_id, + name, + }) + } +} diff --git a/src/entry.rs b/src/entry.rs index 1ed3fec..296ec2f 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -13,11 +13,13 @@ use serde::ser::{self, SerializeStruct, Serializer}; use serde::Serialize; use crate::attribute::header::{MftAttributeHeader, ResidentialHeader}; +use crate::attribute::x30::{FileNameAttr, FileNamespace}; use crate::attribute::{MftAttribute, MftAttributeContent, MftAttributeType}; use std::io::Read; use std::io::SeekFrom; use std::io::{Cursor, Seek}; +use std::path::PathBuf; const SEQUENCE_NUMBER_STRIDE: usize = 512; @@ -150,6 +152,32 @@ impl MftEntry { }) } + /// Retrieves most human-readable representation of a file path entry. + /// Will prefer `Win32` file name attributes, and fallback to `Dos` paths. + pub fn find_best_name_attribute(&self) -> Option<FileNameAttr> { + let file_name_attributes: Vec<FileNameAttr> = self + .iter_attributes() + .filter_map(Result::ok) + .filter_map(|a| a.data.into_file_name()) + .collect(); + + // Try to find a human-readable filename first + let win32_filename = file_name_attributes + .iter() + .find(|a| [FileNamespace::Win32, FileNamespace::Win32AndDos].contains(&a.namespace)); + + match win32_filename { + Some(filename) => Some(filename.clone()), + None => { + // Try to take anything + match file_name_attributes.iter().next() { + Some(filename) => Some(filename.clone()), + None => None, + } + } + } + } + /// Applies the update sequence array fixups. /// https://docs.microsoft.com/en-us/windows/desktop/devnotes/multi-sector-header /// **Note**: The fixup will be written at the end of each 512-byte stride, @@ -91,19 +91,42 @@ impl<T: ReadSeek> MftParser<T> { (0..total_entries).map(move |i| self.get_entry(i)) } + fn inner_get_entry(&mut self, parent_entry_id: u64, entry_name: Option<&str>) -> PathBuf { + let cached_entry = self.entries_cache.cache_get(&parent_entry_id); + + // If my parent path is known, then my path is parent's full path + my name. + // Else, retrieve and cache my parent's path. + if let Some(cached_parent_path) = cached_entry { + match entry_name { + Some(name) => cached_parent_path.clone().join(name), + None => cached_parent_path.clone(), + } + } else { + let path = match self.get_entry(parent_entry_id).ok() { + Some(parent) => match self.get_full_path_for_entry(&parent) { + Ok(Some(path)) => path, + // I have a parent, which doesn't have a filename attribute. + // Default to root. + _ => PathBuf::new(), + }, + // Parent is maybe corrupted or incomplete, use a sentinel instead. + None => PathBuf::from("[Unknown]"), + }; + + self.entries_cache.cache_set(parent_entry_id, path.clone()); + match entry_name { + Some(name) => path.join(name), + None => path, + } + } + } + /// Gets the full path for an entry. /// Caches computations. pub fn get_full_path_for_entry(&mut self, entry: &MftEntry) -> Result<Option<PathBuf>> { let entry_id = entry.header.record_number; - - for attribute in entry.iter_attributes().filter_map(|a| a.ok()) { - if let AttrX30(filename_header) = attribute.data { - if ![FileNamespace::Win32, FileNamespace::Win32AndDos] - .contains(&filename_header.namespace) - { - continue; - } - + match entry.find_best_name_attribute() { + Some(filename_header) => { let parent_entry_id = filename_header.parent.entry; // MFT entry 5 is the root path. @@ -120,27 +143,10 @@ impl<T: ReadSeek> MftParser<T> { } if parent_entry_id > 0 { - let cached_entry = self.entries_cache.cache_get(&parent_entry_id); - - // If my parent path is known, then my path is parent's full path + my name. - // Else, retrieve and cache my parent's path. - if let Some(cached_parent_path) = cached_entry { - return Ok(Some(cached_parent_path.clone().join(filename_header.name))); - } else { - let path = match self.get_entry(parent_entry_id).ok() { - Some(parent) => match self.get_full_path_for_entry(&parent) { - Ok(Some(path)) => path, - // I have a parent, which doesn't have a filename attribute. - // Default to root. - _ => PathBuf::new(), - }, - // Parent is maybe corrupted or incomplete, use a sentinel instead. - None => PathBuf::from("[Unknown]"), - }; - - self.entries_cache.cache_set(parent_entry_id, path.clone()); - return Ok(Some(path.join(filename_header.name))); - } + Ok(Some(self.inner_get_entry( + parent_entry_id, + Some(&filename_header.name), + ))) } else { trace!("Found orphaned entry ID {}", entry_id); @@ -148,12 +154,16 @@ impl<T: ReadSeek> MftParser<T> { self.entries_cache .cache_set(entry.header.record_number, orphan.clone()); - return Ok(Some(orphan)); + + Ok(Some(orphan)) } } + None => match entry.header.base_reference.entry { + // I don't have a parent reference, and no X30 attribute. Though luck. + 0 => Ok(None), + parent_entry_id => Ok(Some(self.inner_get_entry(parent_entry_id, None))), + }, } - - Ok(None) } } |