diff --git a/dependencies/src.sh b/dependencies/src.sh index 0353edf2b..a02cfb53e 100755 --- a/dependencies/src.sh +++ b/dependencies/src.sh @@ -39,3 +39,12 @@ make cp srec2bin /usr/local/bin/ cd /tmp rm -rf /tmp/srec + +# Install latest version of 7z (static) for APFS support +mkdir /tmp/7z +cd /tmp/7z +wget https://www.7-zip.org/a/7z2408-linux-x64.tar.xz +tar -xf 7z2408-linux-x64.tar.xz +cp 7zzs /usr/local/bin/ +cd /tmp +rm -rf /tmp/7z diff --git a/src/binwalk.rs b/src/binwalk.rs index ee0053698..3a32c388b 100644 --- a/src/binwalk.rs +++ b/src/binwalk.rs @@ -277,8 +277,8 @@ impl Binwalk { signature_result.name, FILE_START_OFFSET ); - // Only update the next_valid_offset if confidence is at least medium - if signature_result.confidence >= signatures::common::CONFIDENCE_MEDIUM { + // Only update the next_valid_offset if confidence is high; these are, after all, short signatures + if signature_result.confidence >= signatures::common::CONFIDENCE_HIGH { next_valid_offset = signature_result.offset + signature_result.size; } diff --git a/src/extractors.rs b/src/extractors.rs index f9663ccd5..87cf0104c 100644 --- a/src/extractors.rs +++ b/src/extractors.rs @@ -140,6 +140,7 @@ //! ``` pub mod androidsparse; +pub mod apfs; pub mod arcadyan; pub mod autel; pub mod bzip2; diff --git a/src/extractors/apfs.rs b/src/extractors/apfs.rs new file mode 100644 index 000000000..3445162e9 --- /dev/null +++ b/src/extractors/apfs.rs @@ -0,0 +1,18 @@ +use crate::extractors; + +/// Describes how to run the 7zzs utility to extract APFS images +pub fn apfs_extractor() -> extractors::common::Extractor { + extractors::common::Extractor { + utility: extractors::common::ExtractorType::External("7zzs".to_string()), + extension: "img".to_string(), + arguments: vec![ + "x".to_string(), // Perform extraction + "-y".to_string(), // Assume Yes to all questions + "-o.".to_string(), // Output to current working directory + extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), + ], + // If there is trailing data after the compressed data, extraction will happen but exit code will be 2 + exit_codes: vec![0, 2], + ..Default::default() + } +} diff --git a/src/magic.rs b/src/magic.rs index a728ce7b5..7a6b529f8 100644 --- a/src/magic.rs +++ b/src/magic.rs @@ -910,6 +910,17 @@ pub fn patterns() -> Vec { description: signatures::ntfs::DESCRIPTION.to_string(), extractor: Some(extractors::tsk::tsk_extractor()), }, + // APFS + signatures::common::Signature { + name: "apfs".to_string(), + short: false, + magic_offset: 0, + always_display: false, + magic: signatures::apfs::apfs_magic(), + parser: signatures::apfs::apfs_parser, + description: signatures::apfs::DESCRIPTION.to_string(), + extractor: Some(extractors::apfs::apfs_extractor()), + }, ]; binary_signatures diff --git a/src/signatures.rs b/src/signatures.rs index 3758e2318..f6395cc34 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -107,6 +107,7 @@ //! ``` pub mod aes; pub mod androidsparse; +pub mod apfs; pub mod arcadyan; pub mod autel; pub mod binhdr; diff --git a/src/signatures/apfs.rs b/src/signatures/apfs.rs new file mode 100644 index 000000000..9ccef9b49 --- /dev/null +++ b/src/signatures/apfs.rs @@ -0,0 +1,56 @@ +use crate::signatures::common::{SignatureError, SignatureResult, CONFIDENCE_MEDIUM}; +use crate::structures::apfs::{parse_apfs_header, MAGIC_OFFSET}; + +/// Human readable description +pub const DESCRIPTION: &str = "APple File System"; + +/// APFS magic bytes +pub fn apfs_magic() -> Vec> { + vec![b"NXSB".to_vec()] +} + +/// Validates the APFS header +pub fn apfs_parser(file_data: &[u8], offset: usize) -> Result { + const MBR_BLOCK_SIZE: usize = 512; + + // Successful return value + let mut result = SignatureResult { + description: DESCRIPTION.to_string(), + confidence: CONFIDENCE_MEDIUM, + ..Default::default() + }; + + if offset >= MAGIC_OFFSET { + result.offset = offset - MAGIC_OFFSET; + let available_data = file_data.len() - result.offset; + + if let Ok(apfs_header) = parse_apfs_header(&file_data[result.offset..]) { + let mut truncated_message = "".to_string(); + result.size = apfs_header.block_count * apfs_header.block_size; + + // It is observed that an APFS contained in an EFIGPT with a protective MBR includes the MBR block in its size. + // If the APFS image is pulled out of the EFIGPT, the reported size will be 512 bytes too long, but otherwise valid. + if result.size > available_data { + let truncated_size = result.size - available_data; + + // If the calculated size is 512 bytes short, adjust the reported APFS size accordingly + if truncated_size == MBR_BLOCK_SIZE { + result.size -= truncated_size; + truncated_message = format!(" (truncated by {} bytes)", truncated_size); + } + } + + result.description = format!( + "{}, block size: {} bytes, block count: {}, total size: {} bytes{}", + result.description, + apfs_header.block_size, + apfs_header.block_count, + result.size, + truncated_message + ); + return Ok(result); + } + } + + Err(SignatureError) +} diff --git a/src/structures.rs b/src/structures.rs index e5c55e9b4..f5e2ab764 100644 --- a/src/structures.rs +++ b/src/structures.rs @@ -97,6 +97,7 @@ //! ``` pub mod androidsparse; +pub mod apfs; pub mod autel; pub mod binhdr; pub mod cab; diff --git a/src/structures/apfs.rs b/src/structures/apfs.rs new file mode 100644 index 000000000..f17c69b80 --- /dev/null +++ b/src/structures/apfs.rs @@ -0,0 +1,87 @@ +use crate::structures::common::{self, StructureError}; + +/// Offset of the APFS magic bytes from the start of the APFS image +pub const MAGIC_OFFSET: usize = 0x20; + +/// Struct to store APFS header info +#[derive(Debug, Default, Clone)] +pub struct APFSHeader { + pub block_size: usize, + pub block_count: usize, +} + +/// Parses an APFS header +pub fn parse_apfs_header(apfs_data: &[u8]) -> Result { + const MAX_FS_COUNT: usize = 100; + const FS_COUNT_BLOCK_SIZE: usize = 512; + + // Partial APFS header, just to figure out the size of the image and validate some fields + // https://developer.apple.com/support/downloads/Apple-File-System-Reference.pdf + let apfs_structure = vec![ + ("magic", "u32"), + ("block_size", "u32"), + ("block_count", "u64"), + ("nx_features", "u64"), + ("nx_ro_compat_features", "u64"), + ("nx_incompat_features", "u64"), + ("nx_uuid_p1", "u64"), + ("nx_uuid_p2", "u64"), + ("nx_next_oid", "u64"), + ("nx_next_xid", "u64"), + ("nx_xp_desc_blocks", "u32"), + ("nx_xp_data_blocks", "u32"), + ("nx_xp_desc_base", "u64"), + ("nx_xp_data_base", "u64"), + ("nx_xp_desc_next", "u32"), + ("nx_xp_data_next", "u32"), + ("nx_xp_desc_index", "u32"), + ("nx_xp_desc_len", "u32"), + ("nx_xp_data_index", "u32"), + ("nx_xp_data_len", "u32"), + ("nx_spaceman_oid", "u64"), + ("nx_omap_oid", "u64"), + ("nx_reaper_oid", "u64"), + ("nx_xp_test_type", "u32"), + ("nx_xp_max_file_systems", "u32"), + ]; + + // Expected values of superblock flag fields + let allowed_feature_flags: Vec = vec![0, 1, 2, 3]; + let allowed_incompat_flags: Vec = vec![0, 1, 2, 3, 0x100, 0x101, 0x102, 0x103]; + let allowed_ro_compat_flags: Vec = vec![0]; + + let apfs_struct_start = MAGIC_OFFSET; + let apfs_struct_end = apfs_struct_start + common::size(&apfs_structure); + + // Parse the header + if let Some(apfs_structure_data) = apfs_data.get(apfs_struct_start..apfs_struct_end) { + if let Ok(apfs_header) = common::parse(apfs_structure_data, &apfs_structure, "little") { + // Simple sanity check on the reported block data + if apfs_header["block_size"] != 0 && apfs_header["block_count"] != 0 { + // Sanity check the feature flags + if allowed_feature_flags.contains(&apfs_header["nx_features"]) + && allowed_ro_compat_flags.contains(&apfs_header["nx_ro_compat_features"]) + && allowed_incompat_flags.contains(&apfs_header["nx_incompat_features"]) + { + // The test_type field *must* be NULL + if apfs_header["nx_xp_test_type"] == 0 { + // Calculate the file system count; this is max_file_systems divided by 512, rounded up to nearest whole + let fs_count = ((apfs_header["nx_xp_max_file_systems"] as f32) + / (FS_COUNT_BLOCK_SIZE as f32)) + .ceil() as usize; + + // Sanity check the file system count + if fs_count > 0 && fs_count <= MAX_FS_COUNT { + return Ok(APFSHeader { + block_size: apfs_header["block_size"], + block_count: apfs_header["block_count"], + }); + } + } + } + } + } + } + + Err(StructureError) +}