Add new mtk pkg support and key, needed changes for LZHSfs , add LZ4 support.

This commit is contained in:
theubusu
2025-12-27 17:09:08 +01:00
parent a30cd191e8
commit 5b92b8abfa
9 changed files with 260 additions and 49 deletions
+1 -29
View File
@@ -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 `_<INPUT_TARGET>`.
# 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 |
# Supported formats
+28
View File
@@ -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 |
+1
View File
@@ -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;
+15 -6
View File
@@ -12,11 +12,12 @@ use crate::keys;
struct Header {
#[br(count = 4)] vendor_magic_bytes: Vec<u8>,
#[br(count = 8)] _mtk_magic: Vec<u8>, //#DH@FiRm
#[br(count = 60)] version_bytes: Vec<u8>,
#[br(count = 56)] version_bytes: Vec<u8>,
_unk: u32,
file_size: u32,
_platform: u32,
_flags: u32,
#[br(count = 32)] product_name_bytes: Vec<u8>,
#[br(count = 32)] _unknown: Vec<u8>,
#[br(count = 32)] _digest: Vec<u8>,
}
impl Header {
fn vendor_magic(&self) -> String {
@@ -165,9 +166,17 @@ pub fn extract_mtk_pkg(mut file: &File, output_folder: &str) -> Result<(), Box<d
if part_entry.is_compressed() {
let lzhs_out_path = Path::new(&output_folder).join(part_entry.name() + ".bin");
decompress_lzhs_fs_file2file(&out_file, lzhs_out_path)?;
//after decompression remove the temporary .lzhs file
fs::remove_file(&output_path)?;
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!");
+168
View File
@@ -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<u8>,
#[br(count = 8)] _mtk_magic: Vec<u8>, //#DH@FiRm
#[br(count = 56)] version_bytes: Vec<u8>,
_unk: u32,
file_size: u32,
_flags: u32,
#[br(count = 32)] product_name_bytes: Vec<u8>,
#[br(count = 256)] _digest: Vec<u8>,
}
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<u8>,
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<dyn std::error::Error>> {
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(())
}
+1 -1
View File
@@ -19,7 +19,7 @@ struct Header {
file_size: u32,
_flags: u32,
#[br(count = 32)] product_name_bytes: Vec<u8>,
#[br(count = 32)] _encrypted_digest: Vec<u8>,
#[br(count = 32)] _digest: Vec<u8>,
}
impl Header {
fn vendor_magic(&self) -> String {
+1
View File
@@ -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
];
+4
View File
@@ -144,6 +144,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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)?;
+41 -13
View File
@@ -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<u8>,
#[br(count = 6)] padding: Vec<u8>,
}
pub fn decompress_lzhs_fs_file2file(mut file: &File, output_file: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
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)?;