Skip to content

Commit

Permalink
Merge pull request #815 from rhilseth/master
Browse files Browse the repository at this point in the history
Added support for Matter OTA firmware
  • Loading branch information
devttys0 authored Jan 7, 2025
2 parents 2f6ca77 + 1d01a5f commit ab6636c
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/extractors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ pub mod lz4;
pub mod lzfse;
pub mod lzma;
pub mod lzop;
pub mod matter_ota;
pub mod mbr;
pub mod mh01;
pub mod pcap;
Expand Down
70 changes: 70 additions & 0 deletions src/extractors/matter_ota.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType};
use crate::structures::matter_ota::parse_matter_ota_header;

/// Defines the internal extractor function for extracting a Matter OTA firmware payload */
///
/// ```
/// use std::io::ErrorKind;
/// use std::process::Command;
/// use binwalk::extractors::common::ExtractorType;
/// use binwalk::extractors::matter_ota::matter_ota_extractor;
///
/// match matter_ota_extractor().utility {
/// ExtractorType::None => panic!("Invalid extractor type of None"),
/// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func),
/// ExtractorType::External(cmd) => {
/// if let Err(e) = Command::new(&cmd).output() {
/// if e.kind() == ErrorKind::NotFound {
/// panic!("External extractor '{}' not found", cmd);
/// } else {
/// panic!("Failed to execute external extractor '{}': {}", cmd, e);
/// }
/// }
/// }
/// }
/// ```
pub fn matter_ota_extractor() -> Extractor {
Extractor {
utility: ExtractorType::Internal(extract_matter_ota),
..Default::default()
}
}

/// Matter OTA firmware payload extractor
pub fn extract_matter_ota(
file_data: &[u8],
offset: usize,
output_directory: Option<&str>,
) -> ExtractionResult {
const OUTFILE_NAME: &str = "matter_payload.bin";

let mut result = ExtractionResult {
..Default::default()
};

if let Ok(ota_header) = parse_matter_ota_header(&file_data[offset..]) {
const MAGIC_SIZE: usize = 4;
const TOTAL_SIZE_SIZE: usize = 8;
const HEADER_SIZE_SIZE: usize = 4;

let total_header_size =
MAGIC_SIZE + TOTAL_SIZE_SIZE + HEADER_SIZE_SIZE + ota_header.header_size;

result.success = true;
result.size = Some(ota_header.total_size);

let payload_start = offset + total_header_size;
let payload_end = offset + total_header_size + ota_header.payload_size;

// Sanity check reported payload size and get the payload data
if let Some(payload_data) = file_data.get(payload_start..payload_end) {
if output_directory.is_some() {
let chroot = Chroot::new(output_directory);
result.success =
chroot.carve_file(OUTFILE_NAME, payload_data, 0, payload_data.len());
}
}
}

result
}
11 changes: 11 additions & 0 deletions src/magic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,17 @@ pub fn patterns() -> Vec<signatures::common::Signature> {
description: signatures::encfw::DESCRIPTION.to_string(),
extractor: Some(extractors::encfw::encfw_extractor()),
},
// matter ota firmware
signatures::common::Signature {
name: "matter_ota".to_string(),
short: true,
magic_offset: 0,
always_display: false,
magic: signatures::matter_ota::matter_ota_magic(),
parser: signatures::matter_ota::matter_ota_parser,
description: signatures::matter_ota::DESCRIPTION.to_string(),
extractor: Some(extractors::matter_ota::matter_ota_extractor()),
},
];

binary_signatures
Expand Down
1 change: 1 addition & 0 deletions src/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ pub mod lz4;
pub mod lzfse;
pub mod lzma;
pub mod lzop;
pub mod matter_ota;
pub mod mbr;
pub mod mh01;
pub mod ntfs;
Expand Down
43 changes: 43 additions & 0 deletions src/signatures/matter_ota.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use crate::signatures::common::{SignatureError, SignatureResult, CONFIDENCE_HIGH};
use crate::structures::matter_ota::parse_matter_ota_header;

/// Human readable description
pub const DESCRIPTION: &str = "Matter OTA firmware";

/// Matter OTA firmware images always start with these bytes
pub fn matter_ota_magic() -> Vec<Vec<u8>> {
vec![b"\x1e\xf1\xee\x1b".to_vec()]
}

/// Validates the Matter OTA header
pub fn matter_ota_parser(
file_data: &[u8],
offset: usize,
) -> Result<SignatureResult, SignatureError> {
// Successful return value
let mut result = SignatureResult {
offset,
description: DESCRIPTION.to_string(),
..Default::default()
};

if let Ok(ota_header) = parse_matter_ota_header(&file_data[offset..]) {
result.confidence = CONFIDENCE_HIGH;
result.size = ota_header.header_size;
result.description = format!(
"{}, total size: {} bytes, tlv header size: {} bytes, vendor id: 0x{:x}, product id: 0x{:x}, version: {}, payload size: {} bytes, digest type: {}, payload digest: {}",
result.description,
ota_header.total_size,
ota_header.header_size,
ota_header.vendor_id,
ota_header.product_id,
ota_header.version,
ota_header.payload_size,
ota_header.image_digest_type,
ota_header.image_digest,
);

return Ok(result);
}
Err(SignatureError)
}
1 change: 1 addition & 0 deletions src/structures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ pub mod lz4;
pub mod lzfse;
pub mod lzma;
pub mod lzop;
pub mod matter_ota;
pub mod mbr;
pub mod mh01;
pub mod ntfs;
Expand Down
224 changes: 224 additions & 0 deletions src/structures/matter_ota.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
use std::collections::HashMap;

use crate::common::{get_cstring, is_offset_safe};
use crate::structures::common::{self, StructureError};

/// Struct to store Matter OTA header info
#[derive(Debug, Default, Clone)]
pub struct MatterOTAHeader {
pub total_size: usize,
pub header_size: usize,
pub vendor_id: usize,
pub product_id: usize,
pub version: String,
pub payload_size: usize,
pub image_digest_type: usize,
pub image_digest: String,
}

#[derive(Debug)]
enum Value {
Struct,
EndOfContainer,
Unsigned(usize),
String(String),
OctetString(Vec<u8>),
}

#[derive(Debug)]
struct Element {
tag: Option<usize>,
value: Value,
}

/// Parse a Matter OTA firmware header
pub fn parse_matter_ota_header(ota_data: &[u8]) -> Result<MatterOTAHeader, StructureError> {
let ota_structure = vec![
("magic", "u32"),
("total_size", "u64"),
("header_size", "u32"),
];

if let Ok(ota_header) = common::parse(ota_data, &ota_structure, "little") {
let total_size: usize = ota_header["total_size"];
let header_size: usize = ota_header["header_size"];

// Header starts after the magic, total size and header size fields
let header_start = common::size(&ota_structure);
let header_end = header_start + header_size;
let header_data = ota_data
.get(header_start..header_end)
.ok_or(StructureError)?;

let header = parse_tlv_header(header_data)?;

let mut result = MatterOTAHeader {
total_size,
header_size,
..Default::default()
};

for (key, value) in header.into_iter() {
match (key.as_ref(), value) {
("VendorID", Value::Unsigned(vendor_id)) => result.vendor_id = vendor_id,
("ProductID", Value::Unsigned(product_id)) => result.product_id = product_id,
("SoftwareVersionString", Value::String(version)) => result.version = version,
("PayloadSize", Value::Unsigned(payload_size)) => {
result.payload_size = payload_size
}
("ImageDigestType", Value::Unsigned(image_digest_type)) => {
result.image_digest_type = image_digest_type
}
("ImageDigest", Value::OctetString(image_digest)) => {
let mut digest_string = String::new();
for b in image_digest {
digest_string.push_str(&format!("{:02x}", b));
}
result.image_digest = digest_string;
}
// Ignore other fields
_ => {}
}
}

// Sanity check
if (result.payload_size + header_start + header_size) == total_size {
return Ok(result);
}
}
Err(StructureError)
}

/// Parse tlv element, return result and new offset
fn parse_tlv_element(data: &[u8]) -> Result<(Element, usize), StructureError> {
let control_octet = data.first().ok_or(StructureError)?;

let element_type = control_octet & 0x1f;
let tag_control = control_octet >> 5;

// Lower 2 bits of the control octet determine the field width of integer types
// or the width of the length field for string types
let field_width_type = match element_type & 0x3 {
0 => "u8",
1 => "u16",
2 => "u32",
3 => "u64",
_ => return Err(StructureError),
};

// Parse numerical tag. Only supports anonymous fields and fields with a one byte tag
let (tag, field_offset) = match tag_control {
0 => (None, 1), // Anonymous field
1 => (Some(*data.get(1).ok_or(StructureError)? as usize), 2),
_ => return Err(StructureError),
};

let field_data = data.get(field_offset..).ok_or(StructureError)?;

match element_type {
0b1_0101 => Ok((
// Struct container
Element {
tag,
value: Value::Struct,
},
field_offset,
)),
0b1_1000 => Ok((
// End of container
Element {
tag,
value: Value::EndOfContainer,
},
field_offset,
)),
0b0_0100..=0b0_0111 => {
// Unsigned integer
let structure = &vec![("field", field_width_type)];
let result = common::parse(field_data, structure, "little")?;
Ok((
Element {
tag,
value: Value::Unsigned(result["field"]),
},
field_offset + common::size(structure),
))
}
0b0_1100..=0b0_1111 => {
// UTF-8 String
let structure = &vec![("string_length", field_width_type)];
let result = common::parse(field_data, structure, "little")?;
let string_length = result["string_length"] as usize;
let string_data = field_data
.get(common::size(structure)..)
.ok_or(StructureError)?;
// The string buffer isn't null-terminated, so use the explicit length
let string = string_data
.get(..string_length)
.map(get_cstring)
.ok_or(StructureError)?;
Ok((
Element {
tag,
value: Value::String(string),
},
field_offset + common::size(structure) + string_length,
))
}
0b1_0000..=0b1_0011 => {
// Octet string
let structure = &vec![("octet_string_length", field_width_type)];
let result = common::parse(field_data, structure, "little")?;
let octet_string_length = result["octet_string_length"] as usize;
let octet_string_data = field_data
.get(common::size(structure)..)
.ok_or(StructureError)?;
Ok((
Element {
tag,
value: Value::OctetString(
octet_string_data
.get(..octet_string_length)
.ok_or(StructureError)?
.to_vec(),
),
},
field_offset + common::size(structure) + octet_string_length,
))
}
_ => Err(StructureError), // Other types are not implemented, but not necessary for header parsing
}
}

fn parse_tlv_header(data: &[u8]) -> Result<HashMap<String, Value>, StructureError> {
// Field names for the Matter OTA header indexed by the tag number
let fields = [
"VendorID",
"ProductID",
"SoftwareVersion",
"SoftwareVersionString",
"PayloadSize",
"MinApplicableSoftwareVersion",
"MaxApplicableSoftwareVersion",
"ReleaseNotesURL",
"ImageDigestType",
"ImageDigest",
];
let mut header = HashMap::new();

let available_data: usize = data.len();

let mut last_tlv_offset: Option<usize> = None;
let mut next_tlv_offset: usize = 0;

while is_offset_safe(available_data, next_tlv_offset, last_tlv_offset) {
let (element, new_offset) = parse_tlv_element(&data[next_tlv_offset..])?;
last_tlv_offset = Some(next_tlv_offset);
next_tlv_offset += new_offset;
if let Some(tag) = element.tag {
let field_name = *fields.get(tag).ok_or(StructureError)?;
header.insert(field_name.to_string(), element.value);
}
}
Ok(header)
}
Binary file added tests/inputs/matter_ota.bin
Binary file not shown.
8 changes: 8 additions & 0 deletions tests/matter_ota.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
mod common;

#[test]
fn integration_test() {
const SIGNATURE_TYPE: &str = "matter_ota";
const INPUT_FILE_NAME: &str = "matter_ota.bin";
common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME);
}

0 comments on commit ab6636c

Please # to comment.