Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for Matter OTA firmware #815

Merged
merged 2 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
devttys0 marked this conversation as resolved.
Show resolved Hide resolved

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);
}
Loading