Skip to content
Open
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
47 changes: 47 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,53 @@ pub fn crc32(data: &[u8]) -> u32 {
crc32_v2::crc32(0, data)
}

/// CRC-32 variant used by the Fritz!Box EVA kernel image file signature.
///
/// Differs from the standard [`crc32`] in three ways: bits are processed
/// MSB-first (polynomial 0x04C11DB7, not the reflected form), the CRC starts
/// at 0 instead of 0xFFFFFFFF, and the payload length is folded into the CRC
/// byte-by-byte before the final one's-complement.
///
/// ## Example
///
/// ```
/// use binwalk_ng::common::eva_file_signature_crc32;
///
/// assert_eq!(eva_file_signature_crc32(b""), 0xFFFFFFFF);
/// ```
pub fn eva_file_signature_crc32(data: &[u8]) -> u32 {
const POLY: u32 = 0x04C11DB7;
const TABLE: [u32; 256] = {
let mut table = [0u32; 256];
let mut i = 1;
while i < 256 {
let mut crc = table[i / 2];
let c = (crc >> 31) ^ ((i as u32) & 1);
crc <<= 1;
if c & 1 != 0 {
crc ^= POLY;
}
table[i] = crc;
i += 1;
}
table
};

let mut crc: u32 = 0;
for &byte in data {
crc = (crc << 8) ^ TABLE[((crc >> 24) as u8 ^ byte) as usize];
}

// Fold the payload length into the CRC
let mut length = data.len();
while length > 0 {
crc = (crc << 8) ^ TABLE[((crc >> 24) as u8 ^ (length & 0xFF) as u8) as usize];
length >>= 8;
}

!crc
}

/// Converts an epoch time to a formatted time string.
///
/// ## Example
Expand Down
1 change: 1 addition & 0 deletions src/extractors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ pub mod dtb;
pub mod dumpifs;
pub mod dxbc;
pub mod encfw;
pub mod eva;
pub mod gif;
pub mod gpg;
pub mod gzip;
Expand Down
96 changes: 96 additions & 0 deletions src/extractors/eva.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType};
use crate::structures::eva::{
EVA_LZMA_HEADER_SIZE, EVA_LZMA_STREAM_HEADER, EvaTiRecord, EvaTiRecordKind, TI_HEADER_SIZE,
parse_eva_image,
};

/// Defines the internal extractor for Fritz!Box EVA kernel images
pub fn eva_extractor() -> Extractor {
Extractor {
utility: ExtractorType::Internal(extract_eva),
..Default::default()
}
}

/// Internal EVA kernel image extractor
pub fn extract_eva(
file_data: &[u8],
offset: usize,
output_directory: Option<&str>,
) -> ExtractionResult {
let mut result = ExtractionResult::default();

let Some(data) = file_data.get(offset..) else {
return result;
};
let Ok(image) = parse_eva_image(data) else {
return result;
};

// Refuse to carve garbage even if the signature parser let a weak match through
if !image.primary.checksum_valid || !image.primary.lzma.data_checksum_valid {
return result;
}
if let Some(ref secondary) = image.secondary
&& (!secondary.checksum_valid || !secondary.lzma.data_checksum_valid)
{
return result;
}

let Some(primary_lzma) = reconstruct_lzma_alone(data, &image.primary) else {
return result;
};
let secondary_lzma = match image.secondary.as_ref() {
Some(secondary) => match reconstruct_lzma_alone(data, secondary) {
Some(bytes) => Some(bytes),
None => return result,
},
None => None,
};

let primary_file_name = match image.primary.kind {
EvaTiRecordKind::Primary => "kernel.lzma",
EvaTiRecordKind::Secondary => "kernel_2nd.lzma",
};

if output_directory.is_some() {
let chroot = Chroot::new(output_directory);
if !chroot.create_file(primary_file_name, &primary_lzma) {
return result;
}
if let Some(ref bytes) = secondary_lzma
&& !chroot.create_file("kernel_2nd.lzma", bytes)
{
return result;
}
}

result.success = true;
result.size = Some(image.total_size);
result
}

/// Reconstruct a standard LZMA alone stream from an EVA TI record
///
/// Standard LZMA alone format:
/// ```text
/// [properties: 1 byte]
/// [dict_size: 4 bytes, little-endian]
/// [uncompressed_size: 8 bytes, little-endian] (upper 4 bytes zero-padded)
/// [compressed_data: compressed_len bytes]
/// ```
///
/// The 3 "unknown" padding bytes from the EVA stream header are dropped
fn reconstruct_lzma_alone(image_data: &[u8], record: &EvaTiRecord) -> Option<Vec<u8>> {
let compressed_data_offset =
record.header_offset + TI_HEADER_SIZE + EVA_LZMA_HEADER_SIZE + EVA_LZMA_STREAM_HEADER;
let compressed_data_end = compressed_data_offset.checked_add(record.lzma.compressed_len)?;
let compressed_data = image_data.get(compressed_data_offset..compressed_data_end)?;

let mut out = Vec::with_capacity(13 + compressed_data.len());
out.push(record.lzma.properties);
out.extend_from_slice(&record.lzma.dict_size.to_le_bytes());
out.extend_from_slice(&(record.lzma.uncompressed_len as u64).to_le_bytes());
out.extend_from_slice(compressed_data);
Some(out)
}
11 changes: 11 additions & 0 deletions src/magic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ pub fn patterns() -> Vec<signatures::common::Signature> {
//extractor: Some(extractors::sevenzip::sevenzip_extractor()),
extractor: Some(extractors::lzma::lzma_extractor()),
},
// Fritz!Box EVA kernel image (TI record format, LZMA-compressed)
signatures::common::Signature {
name: "eva".to_string(),
short: false,
magic_offset: 0,
always_display: false,
magic: signatures::eva::eva_magic(),
parser: signatures::eva::eva_parser,
description: signatures::eva::DESCRIPTION.to_string(),
extractor: Some(extractors::eva::eva_extractor()),
},
// bmp
signatures::common::Signature {
name: "bmp".to_string(),
Expand Down
1 change: 1 addition & 0 deletions src/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ pub mod efigpt;
pub mod elf;
pub mod encfw;
pub mod encrpted_img;
pub mod eva;
pub mod ext;
pub mod fat;
pub mod gif;
Expand Down
128 changes: 128 additions & 0 deletions src/signatures/eva.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use crate::signatures::common::{
CONFIDENCE_HIGH, CONFIDENCE_LOW, CONFIDENCE_MEDIUM, SignatureError, SignatureResult,
};
use crate::structures::eva::{EvaTiRecordKind, parse_eva_image};

/// Human readable description
pub const DESCRIPTION: &str = "Fritz!Box EVA kernel image";

/// EVA magic bytes (dual-kernel, primary TI record, secondary TI record)
///
/// The secondary magic is registered to detect isolated secondary-only
/// fragments. Inside a dual-kernel image the scanner will also hit on the
/// embedded secondary magic; that duplicate lands inside the already-validated
/// dual-kernel region and is filtered out by the conflict-resolution pass in
/// `Binwalk::scan`.
pub fn eva_magic() -> Vec<Vec<u8>> {
vec![
// DUAL_KERNEL (little-endian 0xFEED9112)
b"\x12\x91\xed\xfe".to_vec(),
// TI_AR7 primary (little-endian 0xFEED1281)
b"\x81\x12\xed\xfe".to_vec(),
// TI_AR7_2ND secondary (little-endian 0xFEEDB007)
b"\x07\xb0\xed\xfe".to_vec(),
]
}

/// Validates EVA kernel image signatures
pub fn eva_parser(file_data: &[u8], offset: usize) -> Result<SignatureResult, SignatureError> {
let data = file_data.get(offset..).ok_or(SignatureError)?;
let image = parse_eva_image(data).map_err(|_| SignatureError)?;

let secondary_checksums_ok = image
.secondary
.as_ref()
.is_none_or(|s| s.checksum_valid && s.lzma.data_checksum_valid);
let file_signature_ok = image.file_signature.is_none_or(|sig| sig.valid);

let all_checksums_valid = image.primary.checksum_valid
&& image.primary.lzma.data_checksum_valid
&& secondary_checksums_ok
&& (!image.is_dual_kernel || image.dual_checksum_valid)
&& file_signature_ok;

// A bad checksum at offset 0 is usually a real-but-modified image; deeper in a file it's more likely a false 4-byte match
let confidence = if all_checksums_valid {
CONFIDENCE_HIGH
} else if offset == 0 {
CONFIDENCE_MEDIUM
} else {
CONFIDENCE_LOW
};

let kernel_phrase = if image.is_dual_kernel {
"dual-kernel"
} else if image.primary.kind == EvaTiRecordKind::Secondary {
"secondary-kernel fragment"
} else {
"single-kernel"
};
let primary_label = match image.primary.kind {
EvaTiRecordKind::Primary => "primary",
EvaTiRecordKind::Secondary => "secondary",
};
let primary_desc = format!(
"{} load: {:#X}, entry: {:#X}, compressed: {} bytes, uncompressed: {} bytes",
primary_label,
image.primary.load_addr,
image.primary.entry_addr,
image.primary.lzma.compressed_len,
image.primary.lzma.uncompressed_len,
);
let secondary_desc = image
.secondary
.as_ref()
.map(|s| {
format!(
", secondary load: {:#X}, entry: {:#X}, compressed: {} bytes, uncompressed: {} bytes",
s.load_addr, s.entry_addr, s.lzma.compressed_len, s.lzma.uncompressed_len,
)
})
.unwrap_or_default();
let signature_desc = image
.file_signature
.map(|sig| format!(", file signature CRC: {:#010X}", sig.crc))
.unwrap_or_default();
let validation_desc = if all_checksums_valid {
String::new()
} else {
let mut failures: Vec<&str> = Vec::new();
if !image.primary.checksum_valid {
failures.push("primary TI");
}
if !image.primary.lzma.data_checksum_valid {
failures.push("primary LZMA data");
}
if let Some(ref s) = image.secondary {
if !s.checksum_valid {
failures.push("secondary TI");
}
if !s.lzma.data_checksum_valid {
failures.push("secondary LZMA data");
}
}
if image.is_dual_kernel && !image.dual_checksum_valid {
failures.push("dual trailer");
}
if image.file_signature.is_some_and(|sig| !sig.valid) {
failures.push("file signature");
}
format!(", checksum validation failed ({})", failures.join(", "))
};

Ok(SignatureResult {
offset,
size: image.total_size,
confidence,
description: format!(
"{}, {}, {}{}{}{}",
DESCRIPTION,
kernel_phrase,
primary_desc,
secondary_desc,
signature_desc,
validation_desc,
),
..Default::default()
})
}
1 change: 1 addition & 0 deletions src/structures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ pub mod dtb;
pub mod dxbc;
pub mod efigpt;
pub mod elf;
pub mod eva;
pub mod ext;
pub mod fat;
pub mod gif;
Expand Down
Loading