From 30fafbd3fc9127cd40e8da487bafe755dd719085 Mon Sep 17 00:00:00 2001 From: theubusu <80545678+theubusu@users.noreply.github.com> Date: Mon, 11 May 2026 16:21:45 +0200 Subject: [PATCH] mstar: add support for old secure format, add option to keep unknown dest --- README.md | 5 ++ src/formats.rs | 6 ++ src/formats/mstar/mod.rs | 74 +++++++++++++------------ src/formats/mstar_secure_old/include.rs | 19 +++++++ src/formats/mstar_secure_old/mod.rs | 70 +++++++++++++++++++++++ src/utils/aes.rs | 17 +++++- 6 files changed, 156 insertions(+), 35 deletions(-) create mode 100644 src/formats/mstar_secure_old/include.rs create mode 100644 src/formats/mstar_secure_old/mod.rs diff --git a/README.md b/README.md index 58c3375..5f34781 100644 --- a/README.md +++ b/README.md @@ -101,8 +101,13 @@ Tip: if you have split ROM (.ROM-00 and .ROM-01), extract both into the same fol **Used in:** Many MStar-based TVs (Hisense, Toshiba...) **Notes:** All files should be supported, includes lzop, lz4, lzma, sparse_write support **Options:** +`mstar:keep_unknown` - Save data with unknown destination. ※ Support `dump_dec_hdrs` option (will save the script) +## MStar upgrade bin (Secure, old) +**Used in:** Older MStar-based TVs with Secure upgrade mode (encrypted+signed) +**Notes:** Only default upgrade key is supported. This use the extractor above after decrypting. + ## MediaTek BDP **Used in:** Many MediaTek-based Blu-Ray players (LG, Samsung, Philips, Panasonic...) **Notes:** Some older files may fail to extract diff --git a/src/formats.rs b/src/formats.rs index 60ecc42..8b99139 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -8,6 +8,7 @@ pub struct Format { } pub mod mstar; +pub mod mstar_secure_old; pub mod samsung_old; pub mod nvt_timg; pub mod pfl_upg; @@ -58,6 +59,11 @@ pub fn get_registry() -> Vec { detector_func: crate::formats::mstar::is_mstar_file, extractor_func: crate::formats::mstar::extract_mstar, }, + Format { + name: "mstar_secure_old", + detector_func: crate::formats::mstar_secure_old::is_mstar_secure_old_file, + extractor_func: crate::formats::mstar_secure_old::extract_mstar_secure_old, + }, Format { name: "samsung_old", detector_func: crate::formats::samsung_old::is_samsung_old_dir, diff --git a/src/formats/mstar/mod.rs b/src/formats/mstar/mod.rs index f208c2e..0ea2c88 100644 --- a/src/formats/mstar/mod.rs +++ b/src/formats/mstar/mod.rs @@ -126,45 +126,51 @@ pub fn extract_mstar(app_ctx: &AppContext, _ctx: Box) -> Result<(), Box println!("\nPart - Offset: {}, Size: {} --> {}", offset, size, partname); - if partname == "unknown" { - println!("- Unknown destination, skipping!"); - } else { - let data = common::read_file(&file, offset, size.try_into().unwrap())?; - let out_data; - let output_path = Path::new(&app_ctx.output_dir).join(format!("{}.bin", partname)); - - if compression == CompressionType::Lzma { - println!("- Decompressing LZMA..."); - out_data = decompress_lzma(&data)?; - } else if compression == CompressionType::DoubleLzma { - println!("- Decompressing LZMA (Pass 1)..."); - let pass_1 = decompress_lzma(&data)?; - println!("- Decompressing LZMA (Pass 2)..."); - out_data = decompress_lzma(&pass_1)?; - } else if compression == CompressionType::Lz4 { - println!("- Decompressing lz4, expected size: {}", lz4_expect_size); - out_data = decompress_lz4(&data, lz4_expect_size.try_into().unwrap())?; - } else if compression == CompressionType::Lzo { - println!("- Decompessing LZO.."); - unlzop_to_file(&data, output_path)?; - println!("-- Saved file!"); - i += 1; - continue - } else if compression == CompressionType::Sparse { - println!("- Unsparsing..."); - unsparse_to_file(&data, output_path)?; - println!("-- Saved file!"); - i += 1; - continue + let data = common::read_file(&file, offset, size.try_into().unwrap())?; + let out_data; + let output_path = if partname == "unknown" { + if app_ctx.has_option("mstar:keep_unknown") { + println!("- Warning, unknown destination - saving to _unknown_{}.bin", offset); + Path::new(&app_ctx.output_dir).join(format!("_unknown_{}.bin", offset)) } else { - out_data = data; + println!("- Warning, unknown destination - skipping..."); + continue; } + } else { + Path::new(&app_ctx.output_dir).join(format!("{}.bin", partname)) + }; - fs::create_dir_all(&app_ctx.output_dir)?; - let mut out_file = OpenOptions::new().append(true).create(true).open(output_path)?; - out_file.write_all(&out_data)?; + if compression == CompressionType::Lzma { + println!("- Decompressing LZMA..."); + out_data = decompress_lzma(&data)?; + } else if compression == CompressionType::DoubleLzma { + println!("- Decompressing LZMA (Pass 1)..."); + let pass_1 = decompress_lzma(&data)?; + println!("- Decompressing LZMA (Pass 2)..."); + out_data = decompress_lzma(&pass_1)?; + } else if compression == CompressionType::Lz4 { + println!("- Decompressing lz4, expected size: {}", lz4_expect_size); + out_data = decompress_lz4(&data, lz4_expect_size.try_into().unwrap())?; + } else if compression == CompressionType::Lzo { + println!("- Decompessing LZO.."); + unlzop_to_file(&data, output_path)?; println!("-- Saved file!"); + i += 1; + continue + } else if compression == CompressionType::Sparse { + println!("- Unsparsing..."); + unsparse_to_file(&data, output_path)?; + println!("-- Saved file!"); + i += 1; + continue + } else { + out_data = data; } + + fs::create_dir_all(&app_ctx.output_dir)?; + let mut out_file = OpenOptions::new().append(true).create(true).open(output_path)?; + out_file.write_all(&out_data)?; + println!("-- Saved file!"); } i += 1; diff --git a/src/formats/mstar_secure_old/include.rs b/src/formats/mstar_secure_old/include.rs new file mode 100644 index 0000000..fb96dae --- /dev/null +++ b/src/formats/mstar_secure_old/include.rs @@ -0,0 +1,19 @@ +use binrw::BinRead; + +pub static MSTAR_DEFAULT_UPGRADE_KEY: [u8; 16] = [0x00, 0x07, 0xff, 0x41, 0x54, 0x53, 0x4d, 0x92, 0xfc, 0x55, 0xaa, 0x0f, 0xff, 0x01, 0x10, 0xe0]; +pub static CHUNK_ID: &[u8; 8] = b"MSTAR..."; +pub static CHUNK_END: &[u8; 8] = b"...mstar"; + +#[derive(BinRead)] +pub struct ChunkFileFooter { + _chunk_id: [u8; 8], + pub segment_size: u32, + pub file_data_offset: u32, + pub file_data_len: u32, + _file_hash_offset: u32, + _file_hash_len: u32, + _file_signature_offset: u32, + _file_signature_len: u32, + _reserved: [u8; 84], + _chunk_end: [u8; 8], +} \ No newline at end of file diff --git a/src/formats/mstar_secure_old/mod.rs b/src/formats/mstar_secure_old/mod.rs new file mode 100644 index 0000000..c672007 --- /dev/null +++ b/src/formats/mstar_secure_old/mod.rs @@ -0,0 +1,70 @@ +mod include; +use std::any::Any; +use crate::{AppContext, InputTarget}; + +use std::path::Path; +use std::fs::{self, File, OpenOptions}; +use std::io::{Cursor, Write}; +use binrw::BinReaderExt; + +use crate::utils::common; +use crate::utils::aes::decrypt_aes128_ecb; +use crate::formats::mstar::{extract_mstar, is_mstar_file}; +use include::*; + +pub struct MstarSecureCtx { + dec_footer: Vec, +} + +pub fn is_mstar_secure_old_file(app_ctx: &AppContext) -> Result>, Box> { + let file = match app_ctx.file() {Some(f) => f, None => return Ok(None)}; + let file_size = file.metadata()?.len(); + if file_size < 128 { + return Ok(None); + } + let enc_footer = common::read_file(&file, file_size - 128, 128)?; + let dec_footer = decrypt_aes128_ecb(&MSTAR_DEFAULT_UPGRADE_KEY, &enc_footer)?; + + if &dec_footer[0..8] == CHUNK_ID && &dec_footer[120..128] == CHUNK_END { + Ok(Some(Box::new(MstarSecureCtx {dec_footer}))) + } else { + Ok(None) + } +} + +pub fn extract_mstar_secure_old(app_ctx: &AppContext, ctx: Box) -> Result<(), Box> { + let mut file = app_ctx.file().ok_or("Extractor expected file")?; + let ctx = ctx.downcast::().expect("Missing context"); + + let hdr: ChunkFileFooter = Cursor::new(ctx.dec_footer).read_le()?; + println!("Info -\nSegment size: {}\nFile data offset: {}\nFile data len: {}", hdr.segment_size, hdr.file_data_offset, hdr.file_data_len); + + let enc_data = common::read_file(&mut file, hdr.file_data_offset as u64, hdr.file_data_len as usize)?; + + println!("Decrypting..."); + let dec_data = decrypt_aes128_ecb(&MSTAR_DEFAULT_UPGRADE_KEY, &enc_data)?; + + let output_path = Path::new(&app_ctx.output_dir).join("_decrypted.bin"); + fs::create_dir_all(&app_ctx.output_dir)?; + let mut out_file = OpenOptions::new().write(true).create(true).open(&output_path)?; + out_file.write_all(&dec_data)?; + + println!("- Saved decrypted file!"); + + //run standard mstar ext into same directory + let r_out_file = File::open(&output_path)?; + let in_ctx: AppContext = AppContext { + input: InputTarget::File(r_out_file), + output_dir: app_ctx.output_dir.clone(), + options: app_ctx.options.clone() + }; + + //do check just in case and extract + if let Some(result) = is_mstar_file(&in_ctx)? { + extract_mstar(&in_ctx, result)?; + } else { + return Err("detection failed on decrypted data".into()); + } + + Ok(()) +} \ No newline at end of file diff --git a/src/utils/aes.rs b/src/utils/aes.rs index 10b0065..44b822a 100644 --- a/src/utils/aes.rs +++ b/src/utils/aes.rs @@ -1,6 +1,6 @@ use aes::Aes128; -use cbc::{Decryptor, cipher::{block_padding::Pkcs7, block_padding::NoPadding, BlockDecryptMut, KeyIvInit}}; +use cbc::{Decryptor, cipher::{block_padding::Pkcs7, block_padding::NoPadding, BlockDecryptMut, KeyIvInit}}; type Aes128CbcDec = Decryptor; pub fn decrypt_aes128_cbc_pcks7(encrypted_data: &[u8], key: &[u8; 16], iv: &[u8; 16]) -> Result, Box> { @@ -21,4 +21,19 @@ pub fn decrypt_aes128_cbc_nopad(encrypted_data: &[u8], key: &[u8; 16], iv: &[u8; .map_err(|e| format!("UnpadError: {:?}", e))?; Ok(decrypted.to_vec()) +} + +use ecb::{Decryptor as EcbDecryptor, cipher::{KeyInit, generic_array::GenericArray}}; +type Aes128EcbDec = EcbDecryptor; + +pub fn decrypt_aes128_ecb(key: &[u8; 16], ciphertext: &[u8]) -> Result, Box> { + let mut buffer = ciphertext.to_vec(); + + let mut decryptor = Aes128EcbDec::new(key.into()); + for chunk in buffer.chunks_exact_mut(16) { + let block: &mut [u8; 16] = chunk.try_into()?; + decryptor.decrypt_block_mut(GenericArray::from_mut_slice(block)); + } + + Ok(buffer) } \ No newline at end of file