diff --git a/src/extractors.rs b/src/extractors.rs index 04a121260..06243ea7f 100644 --- a/src/extractors.rs +++ b/src/extractors.rs @@ -181,6 +181,7 @@ pub mod srec; pub mod svg; pub mod swapped; pub mod tarball; +pub mod tiff; pub mod trx; pub mod tsk; pub mod ubi; diff --git a/src/extractors/tiff.rs b/src/extractors/tiff.rs new file mode 100644 index 000000000..681eefb08 --- /dev/null +++ b/src/extractors/tiff.rs @@ -0,0 +1,38 @@ +use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; +use crate::structures::tiff::get_tiff_size; + +/// Defines the internal extractor function for carving out TIFF images +pub fn tiff_extractor() -> Extractor { + Extractor { + do_not_recurse: true, + utility: ExtractorType::Internal(extract_tiff_image), + ..Default::default() + } +} + +/// Internal extractor for carving TIFF images to disk +pub fn extract_tiff_image( + file_data: &[u8], + offset: usize, + output_directory: Option<&str>, +) -> ExtractionResult { + const OUTFILE_NAME: &str = "image.tiff"; + + let mut result = ExtractionResult { + ..Default::default() + }; + + // Find the TIFF size + if let Ok(size) = get_tiff_size(&file_data[offset..]) { + result.size = Some(size); + result.success = true; + + if output_directory.is_some() { + let chroot = Chroot::new(output_directory); + result.success = + chroot.carve_file(OUTFILE_NAME, file_data, offset, result.size.unwrap()); + } + } + + result +} diff --git a/src/magic.rs b/src/magic.rs index 6f129ccef..57a008cd4 100644 --- a/src/magic.rs +++ b/src/magic.rs @@ -613,6 +613,17 @@ pub fn patterns() -> Vec { description: signatures::jpeg::DESCRIPTION.to_string(), extractor: Some(extractors::jpeg::jpeg_extractor()), }, + // TIFF image + signatures::common::Signature { + name: "tiff".to_string(), + short: false, + magic_offset: 0, + always_display: false, + magic: signatures::tiff::tiff_magic(), + parser: signatures::tiff::tiff_parser, + description: signatures::tiff::DESCRIPTION.to_string(), + extractor: Some(extractors::tiff::tiff_extractor()), + }, // arcadyan obfuscated lzma signatures::common::Signature { name: "arcadyan".to_string(), diff --git a/src/signatures.rs b/src/signatures.rs index 68c1ac50b..67c66c354 100644 --- a/src/signatures.rs +++ b/src/signatures.rs @@ -186,6 +186,7 @@ pub mod squashfs; pub mod srec; pub mod svg; pub mod tarball; +pub mod tiff; pub mod tplink; pub mod trx; pub mod ubi; diff --git a/src/signatures/tiff.rs b/src/signatures/tiff.rs new file mode 100644 index 000000000..c4e36d675 --- /dev/null +++ b/src/signatures/tiff.rs @@ -0,0 +1,43 @@ +use crate::extractors::tiff::extract_tiff_image; +use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; + +/// Human readable description +pub const DESCRIPTION: &str = "TIFF image"; + +/// TIFF magic bytes +pub fn tiff_magic() -> Vec> { + vec![ + // Little-endian TIFF + b"\x49\x49\x2A\x00".to_vec(), + // Big-endian TIFF + b"\x4D\x4D\x00\x2A".to_vec(), + ] +} + +/// Parse a TIFF image +pub fn tiff_parser(file_data: &[u8], offset: usize) -> Result { + // Successful return value + let mut result = SignatureResult { + offset, + description: DESCRIPTION.to_string(), + confidence: CONFIDENCE_MEDIUM, + ..Default::default() + }; + + // Perform an extraction dry-run + let dry_run = extract_tiff_image(file_data, offset, None); + + // If the dry-run was a success, this is probably a valid TIFF file + if dry_run.success { + // Get the total size of the TIFF + if let Some(tiff_size) = dry_run.size { + // Report signature result + result.size = tiff_size; + result.description = + format!("{}, total size: {} bytes", result.description, result.size); + return Ok(result); + } + } + + Err(SignatureError) +} diff --git a/src/structures.rs b/src/structures.rs index 1e66f5bb9..e9b81bb66 100644 --- a/src/structures.rs +++ b/src/structures.rs @@ -156,6 +156,7 @@ pub mod sevenzip; pub mod shrs; pub mod squashfs; pub mod svg; +pub mod tiff; pub mod tplink; pub mod trx; pub mod ubi; diff --git a/src/structures/tiff.rs b/src/structures/tiff.rs new file mode 100644 index 000000000..e280c6964 --- /dev/null +++ b/src/structures/tiff.rs @@ -0,0 +1,92 @@ +use crate::structures::common::{self, StructureError}; + +/// TIFF magic bytes +const TIFF_MAGIC_LE: u16 = 0x4949; // "II" (little-endian) +const TIFF_MAGIC_BE: u16 = 0x4D4D; // "MM" (big-endian) +const TIFF_VERSION: u16 = 42; + +/// Parse and validate a TIFF file header +pub fn get_tiff_size(tiff_data: &[u8]) -> Result { + let tiff_header_structure = vec![ + ("identifier", "u16"), // 0x4949 for little-endian, 0x4D4D for big-endian + ("version", "u16"), // Should be 42 + ("ifd_offset", "u32"), // Offset to first Image File Directory (IFD) + ]; + + // Try parsing + if tiff_data.len() >= common::size(&tiff_header_structure) { + let identifier = u16::from_le_bytes(tiff_data[..2].try_into().unwrap()); + if identifier != TIFF_MAGIC_LE && identifier != TIFF_MAGIC_BE { + return Err(StructureError); + } + let endian = if identifier == TIFF_MAGIC_LE { "little" } else { "big" }; + if let Ok(tiff_header) = common::parse(tiff_data, &tiff_header_structure, endian) { + if tiff_header["version"] as u16 != TIFF_VERSION { + return Err(StructureError); + } + let ifd_offset = tiff_header["ifd_offset"]; + if let Some(size) = calculate_tiff_size(tiff_data, ifd_offset, endian) { + return Ok(size); + } + } + } + + Err(StructureError) +} + +/// Calculate the total size of a TIFF file by parsing IFDs +fn calculate_tiff_size(tiff_data: &[u8], first_ifd_offset: usize, endian: &str) -> Option { + let mut max_offset = first_ifd_offset; + let mut current_ifd_offset = first_ifd_offset; + + // Follow the IFD chain + while current_ifd_offset > 0 && current_ifd_offset < tiff_data.len() { + let ifd_data = &tiff_data.get(current_ifd_offset..)?; + + let ifd_count_struct = vec![("num_dir_entries", "u16")]; + // Parse number of directory entries + let ifd_count = common::parse(ifd_data, &ifd_count_struct, endian).ok()?; + let num_entries = ifd_count["num_dir_entries"]; + + let tag_structure = vec![ + ("tag_id", "u16"), + ("data_type", "u16"), + ("data_count", "u32"), + ("data_offset", "u32"), + ]; + // Parse each tag entry + for i in 0..num_entries { + let entry_offset = common::size(&ifd_count_struct) + (i * common::size(&tag_structure)); + let tag_data = ifd_data.get(entry_offset..entry_offset + common::size(&tag_structure))?; + let tag = common::parse(tag_data, &tag_structure, endian).ok()?; + + // Calculate size of the data referenced by this tag + let data_type_size = match tag["data_type"] { + 1 | 2 | 6 | 7 => 1, // BYTE, ASCII, SBYTE, UNDEFINED + 3 | 8 => 2, // SHORT, SSHORT + 4 | 9 | 11 => 4, // LONG, SLONG, FLOAT + 5 | 10 | 12 => 8, // RATIONAL, SRATIONAL, DOUBLE + _ => 0, + }; + let data_size = data_type_size * tag["data_count"]; + // Update max_offset if data is stored out-of-line + if data_size > 4 { + let data_offset = tag["data_offset"]; + max_offset = max_offset.max(data_offset + data_size); + } + } + // Calculate offset to next IFD pointer (after all entries) + let next_ifd_offset_pos = common::size(&ifd_count_struct) + (num_entries * common::size(&tag_structure)); + // Get next IFD offset + let next_ifd_offset_data = ifd_data.get(next_ifd_offset_pos..)?; + let next_ifd_struct = vec![("next_ifd_offset", "u32")]; + let next_ifd = common::parse(next_ifd_offset_data, &next_ifd_struct, endian).ok()?; + let ifd_size = next_ifd_offset_pos + common::size(&next_ifd_struct); + + max_offset = max_offset.max(current_ifd_offset + ifd_size); + current_ifd_offset = next_ifd["next_ifd_offset"]; + } + + // Return the end of the last valid data + Some(max_offset) +} \ No newline at end of file diff --git a/tests/inputs/tiff.bin b/tests/inputs/tiff.bin new file mode 100644 index 000000000..1ad2d0ff1 Binary files /dev/null and b/tests/inputs/tiff.bin differ diff --git a/tests/tiff.rs b/tests/tiff.rs new file mode 100644 index 000000000..4b1821d14 --- /dev/null +++ b/tests/tiff.rs @@ -0,0 +1,17 @@ +mod common; + +#[test] +fn integration_test() { + const SIGNATURE_TYPE: &str = "tiff"; + const INPUT_FILE_NAME: &str = "tiff.bin"; + + let expected_signature_offsets: Vec = vec![0, 0x143AEF]; + let expected_extraction_offsets: Vec = vec![0, 0x143AEF]; + + let results = common::run_binwalk(SIGNATURE_TYPE, INPUT_FILE_NAME); + common::assert_results_ok( + results, + expected_signature_offsets, + expected_extraction_offsets, + ); +}