diff --git a/README.md b/README.md index 96dd90f..6b5f65c 100644 --- a/README.md +++ b/README.md @@ -10,32 +10,4 @@ Please note that this project is still mostly under development and errors may o `[OUTPUT_FOLDER]` - Folder to save extracted files to. If an output folder is not provided, extracted files will be saved in folder `_`. -# Supported formats -Please note that some formats depend on decryption keys, and matching ones may not be provided in the tool. If you want to add your own keys, use the `keys.rs` file. -| Format name | Used by | Notes | Thanks to | -| ----------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -| Android OTA payload.bin | Some Android MediaTek TVs | Some compression methods are not supported | https://android.googlesource.com/platform/system/update_engine/+/HEAD/update_metadata.proto | -| BDL | HP Printers | All files should be supported. | - | -| EPK v1 | LG TVs before ~2010 | All files should be supported. | https://github.com/openlgtv/epk2extract | -| EPK v2 | LG TVs since ~2010 | **Depends on keys** - see keys.rs | https://github.com/openlgtv/epk2extract | -| EPK v3 | LG webOS TVs | **Depends on keys** - see keys.rs | https://github.com/openlgtv/epk2extract | -| Funai UPG | Some Funai TVs | Only supports unencrypted/unobfuscated files | - | -| INVINCIBLE_IMAGE | LG Broadcom-based Blu-Ray players | Only version 3 is supported (2011+) | - | -| MSD v1.0 | Samsung TVs 2013-2015 | **Depends on keys** - see keys.rs | https://github.com/bugficks/msddecrypt | -| MSD v1.1 | Samsung TVs 2016+ | **Depends on keys** - see keys.rs | https://github.com/bugficks/msddecrypt | -| Mstar upgrade bin | Many MStar-based TVs (Hisense, Toshiba..) | Most files should be supported | - | -| Mediatek BDP | Many Mediatek-based Blu-Ray players (LG, Samsung, Philips, Panasonic...) | Some older files may not be supported | - | -| Mediatek PKG (Old) | Older Mediatek-based TVs | All files should be supported | - | -| Mediatek PKG | Many Mediatek-based TVs (Hisense, Sony, Panasonic, Philips...) | Newer files with larger header are not supported. **Depends on keys** - see keys.rs | https://github.com/openlgtv/epk2extract | -| Novatek PKG (NFWB) | Some Novatek-based TVs (Philips, LG..) | All files should be supported | https://github.com/openlgtv/epk2extract | -| Novatek TIMG | Later Novatek Based TVs (Philips TitanOS/Hisense) | All files should be supported | - | -| Panasonic Blu-Ray (PANA_DVD) | Panasonic Blu-Ray Players/Recorders | **Depends on keys** - see keys.rs | - | -| Philips UPG | Philips pre-TPVision TVS 2008-2013 | **Depends on keys** - see keys.rs | https://github.com/frederic/pflupg-tool | -| PUP | Sony PlayStation 4/ PlayStation 5 | File has to be decrypted | https://github.com/Zer0xFF/ps4-pup-unpacker | -| Roku | Roku TVs/players | Most files should work, but encrypted images contained within will not be decrypted. | - | -| RUF | Samsung Broadcom-based Blu-Ray players | **Depends on keys** - see keys.rs | - | -| RVP/MVP | Sharp Blu-Ray players/recorders | Only supports older files (XOR encrypted) | - | -| Samsung | Old Samsung TV firmwares pre 2013 | **Depends on keys** - see keys.rs | https://github.com/george-hopkins/samygo-patcher | -| SDDL.SEC | Panasonic TVs 2011+ | All files 2011+ are supported | https://github.com/theubusu/sddl_dec | -| SLP | Samsung Tizen-based NX series cameras | All files should be supported | - | -| Sony BDP | Sony Blu-Ray players | Only platforms up to MSB18 are supported | http://malcolmstagg.com/bdp/s390-firmware.html | \ No newline at end of file +# Supported formats \ No newline at end of file diff --git a/formats.md b/formats.md new file mode 100644 index 0000000..2b0ce78 --- /dev/null +++ b/formats.md @@ -0,0 +1,28 @@ +| Format name | Used by | Notes | Thanks to | +| ----------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| Android OTA payload.bin | Some Android MediaTek TVs | Some compression methods are not supported | https://android.googlesource.com/platform/system/update_engine/+/HEAD/update_metadata.proto | +| BDL | HP Printers | All files should be supported. | - | +| EPK v1 | LG TVs before ~2010 | All files should be supported. | https://github.com/openlgtv/epk2extract | +| EPK v2 | LG TVs since ~2010 | **Depends on keys** - see keys.rs | https://github.com/openlgtv/epk2extract | +| EPK v3 | LG webOS TVs | **Depends on keys** - see keys.rs | https://github.com/openlgtv/epk2extract | +| Funai UPG | Some Funai TVs | Only supports unencrypted/unobfuscated files | - | +| INVINCIBLE_IMAGE | LG Broadcom-based Blu-Ray players | Only version 3 is supported (2011+) | - | +| MSD v1.0 | Samsung TVs 2013-2015 | **Depends on keys** - see keys.rs | https://github.com/bugficks/msddecrypt | +| MSD v1.1 | Samsung TVs 2016+ | **Depends on keys** - see keys.rs | https://github.com/bugficks/msddecrypt | +| Mstar upgrade bin | Many MStar-based TVs (Hisense, Toshiba..) | Most files should be supported | - | +| Mediatek BDP | Many Mediatek-based Blu-Ray players (LG, Samsung, Philips, Panasonic...) | Some older files may not be supported | - | +| Mediatek PKG (New) | Newer Mediatek-based TVs | **Depends on keys** - see keys.rs | - | +| Mediatek PKG (Old) | Older Mediatek-based TVs | All files should be supported | - | +| Mediatek PKG | Many Mediatek-based TVs (Hisense, Sony, Panasonic, Philips...) | Some firmwares **Depend on keys** - see keys.rs | https://github.com/openlgtv/epk2extract | +| Novatek PKG (NFWB) | Some Novatek-based TVs (Philips, LG..) | All files should be supported | https://github.com/openlgtv/epk2extract | +| Novatek TIMG | Later Novatek Based TVs (Philips TitanOS/Hisense) | All files should be supported | - | +| Panasonic Blu-Ray (PANA_DVD) | Panasonic Blu-Ray Players/Recorders | **Depends on keys** - see keys.rs | - | +| Philips UPG | Philips pre-TPVision TVS 2008-2013 | **Depends on keys** - see keys.rs | https://github.com/frederic/pflupg-tool | +| PUP | Sony PlayStation 4/ PlayStation 5 | File has to be decrypted | https://github.com/Zer0xFF/ps4-pup-unpacker | +| Roku | Roku TVs/players | Most files should work, but encrypted images contained within will not be decrypted. | - | +| RUF | Samsung Broadcom-based Blu-Ray players | **Depends on keys** - see keys.rs | - | +| RVP/MVP | Sharp Blu-Ray players/recorders | Only supports older files (XOR encrypted) | - | +| Samsung | Old Samsung TV firmwares pre 2013 | **Depends on keys** - see keys.rs | https://github.com/george-hopkins/samygo-patcher | +| SDDL.SEC | Panasonic TVs 2011+ | All files 2011+ are supported | https://github.com/theubusu/sddl_dec | +| SLP | Samsung Tizen-based NX series cameras | All files should be supported | - | +| Sony BDP | Sony Blu-Ray players | Only platforms up to MSB18 are supported | http://malcolmstagg.com/bdp/s390-firmware.html | \ No newline at end of file diff --git a/src/formats.rs b/src/formats.rs index e43a812..9fce3b9 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -28,4 +28,5 @@ pub mod epk3; pub mod mtk_pkg; pub mod mtk_pkg_old; +pub mod mtk_pkg_new; pub mod mtk_bdp; \ No newline at end of file diff --git a/src/formats/mtk_pkg.rs b/src/formats/mtk_pkg.rs index 5c8f8de..8f0f6be 100644 --- a/src/formats/mtk_pkg.rs +++ b/src/formats/mtk_pkg.rs @@ -12,11 +12,12 @@ use crate::keys; struct Header { #[br(count = 4)] vendor_magic_bytes: Vec, #[br(count = 8)] _mtk_magic: Vec, //#DH@FiRm - #[br(count = 60)] version_bytes: Vec, + #[br(count = 56)] version_bytes: Vec, + _unk: u32, file_size: u32, - _platform: u32, + _flags: u32, #[br(count = 32)] product_name_bytes: Vec, - #[br(count = 32)] _unknown: Vec, + #[br(count = 32)] _digest: Vec, } impl Header { fn vendor_magic(&self) -> String { @@ -165,9 +166,17 @@ pub fn extract_mtk_pkg(mut file: &File, output_folder: &str) -> Result<(), Box { + println!("- Decompressed Successfully!"); + //after successfull decompression remove the temporary .lzhs file + fs::remove_file(&output_path)?; + }, + Err(e) => { + eprintln!("Failed to decompress partition!, Error: {}. Saving compressed data...", e); + //if the decompression is not successfull leave out compressed data. + } + } } println!("-- Saved file!"); diff --git a/src/formats/mtk_pkg_new.rs b/src/formats/mtk_pkg_new.rs new file mode 100644 index 0000000..45025c6 --- /dev/null +++ b/src/formats/mtk_pkg_new.rs @@ -0,0 +1,168 @@ +use std::path::Path; +use std::fs::{self, File, OpenOptions}; +use std::io::{Write, Cursor, Seek}; +use binrw::{BinRead, BinReaderExt}; + +use crate::utils::common; +use crate::utils::aes::{decrypt_aes128_cbc_nopad}; +use crate::utils::lzhs::{decompress_lzhs_fs_file2file}; +use crate::keys; + +#[derive(BinRead)] +struct Header { + #[br(count = 4)] vendor_magic_bytes: Vec, + #[br(count = 8)] _mtk_magic: Vec, //#DH@FiRm + #[br(count = 56)] version_bytes: Vec, + _unk: u32, + file_size: u32, + _flags: u32, + #[br(count = 32)] product_name_bytes: Vec, + #[br(count = 256)] _digest: Vec, +} +impl Header { + fn vendor_magic(&self) -> String { + common::string_from_bytes(&self.vendor_magic_bytes) + } + fn version(&self) -> String { + common::string_from_bytes(&self.version_bytes) + } + fn product_name(&self) -> String { + common::string_from_bytes(&self.product_name_bytes) + } + +} + +#[derive(BinRead)] +struct PartEntry { + #[br(count = 4)] name_bytes: Vec, + flags: u32, + size: u32, +} +impl PartEntry { + fn name(&self) -> String { + common::string_from_bytes(&self.name_bytes) + } + fn is_valid(&self) -> bool { + self.name().is_ascii() + } + fn is_encrypted(&self) -> bool { + (self.flags & 1 << 0) != 0 + } + fn is_compressed(&self) -> bool { //lzhs fs + (self.flags & 1 << 8) != 0 + } +} + +pub fn is_mtk_pkg_new_file(file: &File) -> bool { + let encrypted_header = common::read_file(&file, 0, 112).expect("Failed to read from file."); + for (key_hex, iv_hex, _name) in keys::MTK_PKG_CUST { + let key_array: [u8; 16] = hex::decode(key_hex).expect("Error").as_slice().try_into().expect("Error"); + let iv_array: [u8; 16] = hex::decode(iv_hex).expect("Error").as_slice().try_into().expect("Error"); + let try_decrypt = decrypt_aes128_cbc_nopad(&encrypted_header, &key_array, &iv_array).expect("Failed to decrypt."); + + if &try_decrypt[4..12] == b"#DH@FiRm" { + return true; + } + } + + false +} + +pub fn extract_mtk_pkg_new(mut file: &File, output_folder: &str) -> Result<(), Box> { + let file_size = file.metadata()?.len(); + let encrypted_header = common::read_exact(&mut file, 368)?; + + let mut header = Vec::new(); + let mut matching_key: Option<[u8; 16]> = None; + let mut matching_iv: Option<[u8; 16]> = None; + //find key, the header and data key will be the same + for (key_hex, iv_hex, name) in keys::MTK_PKG_CUST { + let key_array = hex::decode(key_hex)?.as_slice().try_into()?; + let iv_array = hex::decode(iv_hex)?.as_slice().try_into()?; + header = decrypt_aes128_cbc_nopad(&encrypted_header, &key_array, &iv_array)?; + + if &header[4..12] == b"#DH@FiRm" { + println!("Using key {}", name); + matching_key = Some(key_array); + matching_iv = Some(iv_array); + break + } + } + if matching_key.is_none() && matching_iv.is_none() { + println!("Failed to find key!"); + return Ok(()) + } + + let mut hdr_reader = Cursor::new(header); + let hdr: Header = hdr_reader.read_le()?; + + println!("File info:\nFile size: {}\nVendor magic: {}\nVersion info: {}\nProduct name: {}" , + hdr.file_size, hdr.vendor_magic(), hdr.version(), hdr.product_name()); + + let mut part_n = 0; + while file.stream_position()? < file_size as u64 { + part_n += 1; + let part_entry: PartEntry = file.read_le()?; + if !part_entry.is_valid() { + break + } + + println!("\n#{} - {}, Size: {}{} {}", + part_n, part_entry.name(), part_entry.size, if part_entry.is_compressed() {" [COMPRESSED]"} else {""}, if part_entry.is_encrypted() {"[ENCRYPTED]"} else {""} ); + + let data = common::read_exact(&mut file, part_entry.size as usize + 48)?; + + if part_entry.size == 0 { + println!("- Empty entry, skipping!"); + continue + } + + let out_data; + if part_entry.is_encrypted() { + println!("- Decrypting..."); + out_data = decrypt_aes128_cbc_nopad(&data[..data.len() & !15], &matching_key.unwrap(), &matching_iv.unwrap())?; + } else { + out_data = data; + } + + //strip iMtK thing and get version + let extra_header_len = if &out_data[48..52] == b"iMtK" { + let imtk_len = u32::from_le_bytes(out_data[52..56].try_into().unwrap()); + if &out_data[56..60] != b"iPAd" { + let version_len = u32::from_le_bytes(out_data[56..60].try_into().unwrap()); + let version = common::string_from_bytes(&out_data[60..60 + version_len as usize]); + println!("- Version: {}", version); + } + imtk_len + 8 + } else { + 0 + }; + + //for compressed part create temp file + let output_path = Path::new(&output_folder).join(part_entry.name() + if part_entry.is_compressed() {".lzhs"} else {".bin"}); + fs::create_dir_all(&output_folder)?; + let mut out_file = OpenOptions::new().write(true).read(true)/* for lzhs */.create(true).open(&output_path)?; + out_file.write_all(&out_data[48 + extra_header_len as usize..])?; + + if part_entry.is_compressed() { + let lzhs_out_path = Path::new(&output_folder).join(part_entry.name() + ".bin"); + match decompress_lzhs_fs_file2file(&out_file, lzhs_out_path) { + Ok(()) => { + println!("-- Decompressed Successfully!"); + //after successfull decompression remove the temporary .lzhs file + fs::remove_file(&output_path)?; + }, + Err(e) => { + eprintln!("Failed to decompress partition!, Error: {}. Saving compressed data...", e); + //if the decompression is not successfull leave out compressed data. + } + } + } + + println!("-- Saved file!"); + } + + println!("\nExtraction finished!"); + + Ok(()) +} \ No newline at end of file diff --git a/src/formats/mtk_pkg_old.rs b/src/formats/mtk_pkg_old.rs index 4ca6b06..65ebeb5 100644 --- a/src/formats/mtk_pkg_old.rs +++ b/src/formats/mtk_pkg_old.rs @@ -19,7 +19,7 @@ struct Header { file_size: u32, _flags: u32, #[br(count = 32)] product_name_bytes: Vec, - #[br(count = 32)] _encrypted_digest: Vec, + #[br(count = 32)] _digest: Vec, } impl Header { fn vendor_magic(&self) -> String { diff --git a/src/keys.rs b/src/keys.rs index 0f1d5d8..3968be4 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -98,4 +98,5 @@ pub static MTK_PKG_CUST: &[(&str, &str, &str)] = &[ ("47FBF8CAD62BB95AF3AD9509E5C2175D", "63120FB321B0410F216D6DC2D8641A11", "Philips 2013"), ("1B569AA7D2E4CCE66584A7A3D8A45679", "A0E88D5D52A813260D3A34A14AA89416", "TPV_MTK2K17PLF_EU0"), ("135AFB6DE91CD56496244BC7C0E08D63", "C6A38C89F0AF5637EB6E19D35E12E257", "TPV_MTK2K14PLF_EU1"), + ("B1BFAA407F70C80C650379DFEAFAA40F", "BC1443A0D17AAB2DB1EA0302EF280717", "Common"), //new mtk pkg, worked for Hisense/CVT/Changhong ]; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index dec4c59..7cf3776 100644 --- a/src/main.rs +++ b/src/main.rs @@ -144,6 +144,10 @@ fn main() -> Result<(), Box> { println!("MTK PKG (Old) file detected!"); formats::mtk_pkg_old::extract_mtk_pkg_old(&file, &output_path)?; } + else if formats::mtk_pkg_new::is_mtk_pkg_new_file(&file) { + println!("MTK PKG (New) file detected!"); + formats::mtk_pkg_new::extract_mtk_pkg_new(&file, &output_path)?; + } else if formats::mtk_bdp::is_mtk_bdp_file(&file) { println!("MTK BDP file detected!"); formats::mtk_bdp::extract_mtk_bdp(&file, &output_path)?; diff --git a/src/utils/lzhs.rs b/src/utils/lzhs.rs index 6e8ec26..b8933be 100644 --- a/src/utils/lzhs.rs +++ b/src/utils/lzhs.rs @@ -4,18 +4,19 @@ use binrw::{BinRead, BinReaderExt}; use std::path::{PathBuf}; use crate::utils::huffman_tables::{CHARLEN, POS}; +use crate::utils::compression::{decompress_lz4}; #[derive(BinRead)] struct LzhsHeader { uncompressed_size: u32, compressed_size: u32, checksum_or_seg_idx: u16, //as checksum in normal lzhs header, as index in lzhs_fs header - #[br(count = 6)] _padding: Vec, + #[br(count = 6)] padding: Vec, } pub fn decompress_lzhs_fs_file2file(mut file: &File, output_file: PathBuf) -> Result<(), Box> { let file_size = file.metadata()?.len(); - let mut out_file = OpenOptions::new().append(true).create(true).open(output_file)?; + let mut out_file = OpenOptions::new().append(true).create(true).open(&output_file)?; file.seek(SeekFrom::Start(0))?; let mut uncompressed_heading = vec![0u8; 0x100000]; //first 1mb is uncompressed @@ -25,23 +26,50 @@ pub fn decompress_lzhs_fs_file2file(mut file: &File, output_file: PathBuf) -> Re while file.stream_position().unwrap() < file_size { let segment_header: LzhsHeader = file.read_le()?; let lzhs_header: LzhsHeader = file.read_le()?; - println!("[LZHS] Segment {} - Compressed size: {}, Decompressed size: {}, Expected Checksum: 0x{:02x?}", - segment_header.checksum_or_seg_idx, lzhs_header.compressed_size, lzhs_header.uncompressed_size, lzhs_header.checksum_or_seg_idx); + + //lz4 type uses a 4 byte checksum instead of 1(2) byte , so the padding is not 0 anymore + //maybe this method will be changed + let is_lz4 = if lzhs_header.padding != b"\x00\x00\x00\x00\x00\x00" {true} else {false}; + + println!("[{}] Segment {} - Compressed size: {}, Decompressed size: {}, Expected Checksum: 0x{:02x?}", + if is_lz4 {"LZ4"} else {"LZHS"}, segment_header.checksum_or_seg_idx, lzhs_header.compressed_size, lzhs_header.uncompressed_size, lzhs_header.checksum_or_seg_idx); let mut compressed_data = vec![0u8; lzhs_header.compressed_size as usize]; file.read_exact(&mut compressed_data)?; + let mut out_data; + if lzhs_header.compressed_size == 0 { + //odd variant no.1: if the compressed size is 0 , the output is just zeros with the uncompressed size. + out_data = vec![0; lzhs_header.uncompressed_size as usize]; - println!("- Decompressing..."); - let out_huff = unhuff(&compressed_data); - let mut out_data = unlzss(&out_huff, lzhs_header.uncompressed_size as usize); - arm_thumb_convert(&mut out_data, 0, false); + } else if (lzhs_header.uncompressed_size > segment_header.uncompressed_size) && + (lzhs_header.compressed_size == lzhs_header.uncompressed_size) && + lzhs_header.checksum_or_seg_idx == 0x00 + { + //odd variant no.2: the lzhs header compressed size is for some reason bigger by 8 bytes (and those 8 bytes are zeros), the data is stored UNCOMPRESSED in these + //the compressed and uncompressed size are the same and the checksum is 0. + out_data = compressed_data[..segment_header.uncompressed_size as usize].to_vec(); - let checksum = calc_checksum(&out_data); - println!("-- Calculated checksum: 0x{:02x?}", checksum); - if u16::from(checksum) != lzhs_header.checksum_or_seg_idx { - println!("--- Checksum mismatch! Expected: 0x{:02x?}, Got: 0x{:02x?}!", lzhs_header.checksum_or_seg_idx, checksum); } else { - println!("--- Checksum OK!") + //normal variant + println!("- Decompressing..."); + if is_lz4 { + out_data = decompress_lz4(&compressed_data, lzhs_header.uncompressed_size as i32)?; + } else { + //lzhs + let out_huff = unhuff(&compressed_data); + out_data = unlzss(&out_huff, lzhs_header.uncompressed_size as usize); + arm_thumb_convert(&mut out_data, 0, false); + + let checksum = calc_checksum(&out_data); + println!("-- Calculated checksum: 0x{:02x?}", checksum); + if u16::from(checksum) != lzhs_header.checksum_or_seg_idx { + println!("--- Checksum mismatch! Expected: 0x{:02x?}, Got: 0x{:02x?}!", lzhs_header.checksum_or_seg_idx, checksum); + return Err("Checksum mismatch!".into()); + } else { + println!("--- Checksum OK!") + } + } + } out_file.write_all(&out_data)?;