diff --git a/formats.md b/formats.md index 225f646..bfbdda5 100644 --- a/formats.md +++ b/formats.md @@ -10,8 +10,8 @@ | 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 | -| Mediatek upgrade_loader | Older Mediatek-based TVs | All files should be supported providing they are not encrypted | - | | 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 | - | diff --git a/src/formats.rs b/src/formats.rs index 4ad7201..30c7943 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -26,5 +26,5 @@ pub mod epk2; pub mod epk3; pub mod mtk_pkg; -pub mod mtk_upgrade_loader; +pub mod mtk_pkg_old; 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 0f52165..86cc2fc 100644 --- a/src/formats/mtk_pkg.rs +++ b/src/formats/mtk_pkg.rs @@ -139,9 +139,12 @@ pub fn extract_mtk_pkg(mut file: &File, output_folder: &str) -> Result<(), Box, + #[br(count = 8)] _mtk_magic: Vec, //#DH@FiRm + #[br(count = 68)] version_bytes: Vec, + file_size: u32, + _flags: u32, + #[br(count = 32)] product_name_bytes: Vec, + #[br(count = 32)] _encrypted_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_encrypted(&self) -> bool { + (self.flags & 1 << 0) == 1 << 0 + } +} + +pub fn is_mtk_pkg_old_file(file: &File) -> bool { + let encrypted_header = common::read_file(&file, 0, 152).expect("Failed to read from file."); + let header = decrypt(&encrypted_header, KEY, Some(HEADER_XOR_MASK)); + if &header[4..12] == b"#DH@FiRm" { + true + } else { + false + } +} + +pub fn extract_mtk_pkg_old(mut file: &File, output_folder: &str) -> Result<(), Box> { + let file_size = file.metadata()?.len(); + let encrypted_header = common::read_exact(&mut file, 152)?; + let header = decrypt(&encrypted_header, KEY, Some(HEADER_XOR_MASK)); + 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()?; + let is_encrypted = if (part_entry.flags & 1 << 0) == 1 << 0 {true} else {false}; + + println!("\n#{} - {}, Size: {} {}", part_n, part_entry.name(), part_entry.size, if is_encrypted {"[ENCRYPTED]"} else {""} ); + + let data = common::read_exact(&mut file, part_entry.size as usize)?; + let out_data; + if part_entry.is_encrypted() { + //decrypt with the vendor magic + println!("- Decrypting..."); + let vendor_magic_u32 = u32::from_le_bytes(hdr.vendor_magic_bytes.clone().try_into().unwrap()); + out_data = decrypt(&data, vendor_magic_u32, Some(CONTENT_XOR_MASK)); + } else { + out_data = data; + } + + //strip iMtK thing and get version + let extra_header_len = if &out_data[0..4] == b"iMtK" { + let imtk_len = u32::from_le_bytes(out_data[4..8].try_into().unwrap()); + let version_len = u32::from_le_bytes(out_data[8..12].try_into().unwrap()); + let version = common::string_from_bytes(&out_data[12..12 + version_len as usize]); + println!("- Version: {}", version); + imtk_len + 8 + } else { + 0 + }; + + let output_path = Path::new(&output_folder).join(part_entry.name() + ".bin"); + fs::create_dir_all(&output_folder)?; + let mut out_file = OpenOptions::new() + .write(true) + .create(true) + .open(output_path)?; + out_file.write_all(&out_data[extra_header_len as usize..])?; + + println!("-- Saved file!"); + } + + println!("\nExtraction finished!"); + + Ok(()) +} \ No newline at end of file diff --git a/src/formats/mtk_upgrade_loader.rs b/src/formats/mtk_upgrade_loader.rs deleted file mode 100644 index 15392bd..0000000 --- a/src/formats/mtk_upgrade_loader.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::path::Path; -use std::fs::{self, File, OpenOptions}; -use std::io::{Write, Seek}; -use binrw::{BinRead, BinReaderExt}; - -use crate::utils::common; - -pub fn is_mtk_upgrade_loader_file(file: &File) -> bool { - let header = common::read_file(&file, 152, 4).expect("Failed to read from file."); - if header == b"cfig" || header == b"load" { //cfig or load is always(?) the first partition in upgrade_loader - true - } else { - false - } -} - -//This format is similar to mtk_pkg, but has different header size and key. It also doesnt have the crypted headers - -#[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) - } -} - -pub fn extract_mtk_upgrade_loader(mut file: &File, output_folder: &str) -> Result<(), Box> { - let file_size = file.metadata()?.len(); - - let mut part_n = 0; - while file.stream_position()? < file_size as u64 { - part_n += 1; - let part_entry: PartEntry = file.read_le()?; - let is_encrypted = if (part_entry.flags & 1 << 0) == 1 << 0 {true} else {false}; - - println!("\n#{} - {}, Size: {} {}", part_n, part_entry.name(), part_entry.size, if is_encrypted {"[ENCRYPTED]"} else {""} ); - - let data = common::read_exact(&mut file, part_entry.size as usize)?; - - //strip iMtK thing - let extra_header_len = if &data[0..4] == b"iMtK" { - let imtk_len = u32::from_le_bytes(data[4..8].try_into().unwrap()); - imtk_len + 8 - } else { - 0 - }; - - //println!("Extra header size: {}", extra_header_len); - - let output_path = Path::new(&output_folder).join(part_entry.name() + ".bin"); - - fs::create_dir_all(&output_folder)?; - let mut out_file = OpenOptions::new() - .write(true) - .create(true) - .open(output_path)?; - - out_file.write_all(&data[extra_header_len as usize..])?; - - println!("-- Saved file!"); - } - - println!("\nExtraction finished!"); - - Ok(()) -} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 1728f74..fcb4230 100644 --- a/src/main.rs +++ b/src/main.rs @@ -133,12 +133,12 @@ fn main() -> Result<(), Box> { formats::roku::extract_roku(&file, &output_path)?; } else if formats::mtk_pkg::is_mtk_pkg_file(&file) { - println!("MTK Pkg file detected!"); + println!("MTK PKG file detected!"); formats::mtk_pkg::extract_mtk_pkg(&file, &output_path)?; } - else if formats::mtk_upgrade_loader::is_mtk_upgrade_loader_file(&file) { - println!("MTK upgrade_loader file detected!"); - formats::mtk_upgrade_loader::extract_mtk_upgrade_loader(&file, &output_path)?; + else if formats::mtk_pkg_old::is_mtk_pkg_old_file(&file) { + println!("MTK PKG (Old) file detected!"); + formats::mtk_pkg_old::extract_mtk_pkg_old(&file, &output_path)?; } else if formats::mtk_bdp::is_mtk_bdp_file(&file) { println!("MTK BDP file detected!"); diff --git a/src/utils.rs b/src/utils.rs index af23844..649e275 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,4 +5,5 @@ pub mod lzss; pub mod lzop; pub mod sparse; pub mod compression; -pub mod android_ota_update_metadata; \ No newline at end of file +pub mod android_ota_update_metadata; +pub mod mtk_crypto; \ No newline at end of file diff --git a/src/utils/mtk_crypto.rs b/src/utils/mtk_crypto.rs new file mode 100644 index 0000000..214083e --- /dev/null +++ b/src/utils/mtk_crypto.rs @@ -0,0 +1,138 @@ +//dword_78A9B0 +const P_TABLE_1: [u8; 24] = [ + 0x00, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0C, 0x0B, + 0x0A, 0x09, 0x08, 0x07, 0x08, 0x07, 0x06, 0x05, + 0x04, 0x03, 0x04, 0x03, 0x02, 0x01, 0x00, 0x0F, +]; + +//dword_78AA10 +const S_BOX: [u8; 256] = [ + 0x0D, 0x06, 0x00, 0x0A, 0x0E, 0x03, 0x0B, 0x05, 0x07, 0x01, 0x09, 0x04, 0x02, 0x08, 0x0C, 0x0F, + 0x00, 0x05, 0x0A, 0x03, 0x07, 0x09, 0x0C, 0x0F, 0x0B, 0x02, 0x06, 0x0D, 0x08, 0x0E, 0x01, 0x04, + 0x08, 0x03, 0x05, 0x09, 0x0B, 0x0C, 0x06, 0x0A, 0x01, 0x0D, 0x02, 0x0E, 0x04, 0x07, 0x0F, 0x00, + 0x07, 0x00, 0x09, 0x05, 0x0C, 0x06, 0x0A, 0x03, 0x08, 0x0B, 0x0F, 0x02, 0x01, 0x0D, 0x04, 0x0E, + 0x09, 0x0E, 0x05, 0x00, 0x0C, 0x07, 0x06, 0x0B, 0x02, 0x04, 0x0F, 0x03, 0x01, 0x0A, 0x08, 0x0D, + 0x0F, 0x02, 0x03, 0x09, 0x06, 0x0C, 0x08, 0x05, 0x01, 0x0D, 0x04, 0x0A, 0x0B, 0x07, 0x0E, 0x00, + 0x05, 0x0B, 0x09, 0x06, 0x0A, 0x01, 0x00, 0x0C, 0x0E, 0x08, 0x02, 0x0F, 0x07, 0x04, 0x0D, 0x03, + 0x0A, 0x05, 0x00, 0x0C, 0x0D, 0x02, 0x07, 0x09, 0x04, 0x03, 0x0B, 0x06, 0x0E, 0x08, 0x01, 0x0F, + 0x0C, 0x02, 0x05, 0x0B, 0x03, 0x0E, 0x0F, 0x04, 0x07, 0x08, 0x09, 0x06, 0x00, 0x0D, 0x0A, 0x01, + 0x07, 0x0E, 0x0A, 0x05, 0x0C, 0x02, 0x01, 0x0B, 0x00, 0x03, 0x0F, 0x08, 0x09, 0x04, 0x06, 0x0D, + 0x01, 0x0F, 0x0B, 0x0C, 0x0E, 0x05, 0x08, 0x02, 0x0A, 0x06, 0x04, 0x03, 0x09, 0x00, 0x07, 0x0D, + 0x08, 0x02, 0x04, 0x0B, 0x07, 0x0C, 0x0D, 0x01, 0x05, 0x0F, 0x03, 0x06, 0x0E, 0x09, 0x00, 0x0A, + 0x0E, 0x02, 0x07, 0x0C, 0x0B, 0x05, 0x04, 0x09, 0x08, 0x0D, 0x01, 0x0A, 0x06, 0x00, 0x0F, 0x03, + 0x04, 0x08, 0x02, 0x05, 0x0E, 0x03, 0x01, 0x0F, 0x0D, 0x07, 0x0B, 0x0C, 0x00, 0x09, 0x06, 0x0A, + 0x09, 0x0E, 0x0A, 0x01, 0x0C, 0x02, 0x07, 0x04, 0x03, 0x00, 0x0F, 0x06, 0x05, 0x0B, 0x08, 0x0D, + 0x0F, 0x04, 0x0C, 0x0B, 0x05, 0x08, 0x02, 0x01, 0x0A, 0x09, 0x06, 0x00, 0x03, 0x0E, 0x0D, 0x07, +]; + +//dword_78AE10 +const P_TABLE_2: [u8; 16] = [ + 0x0C, 0x08, 0x05, 0x00, 0x0A, 0x02, 0x0E, 0x07, + 0x04, 0x09, 0x01, 0x0D, 0x03, 0x06, 0x0B, 0x0F, +]; + + +//sub_379C90 +fn permute_24bit(input: u16, table: &[u8]) -> u32 { + let mut output = 0u32; + for (i, &bit_pos) in table.iter().enumerate().take(24) { + if bit_pos < 32 && ((input >> bit_pos) & 1) != 0 { + output |= 1 << i; + } + } + output +} + +//sub_379CB8 +fn sbox_substitute(input: u32) -> u16 { + let mut output = 0u16; + for i in 0..4 { + let bits6 = ((input >> (i * 6)) & 0x3F) as usize; + let row_offset = 64 * i; + let index = row_offset + bits6; + let sbox_value = (S_BOX[index] & 0x0F) as u16; + + output |= sbox_value << (4 * i); + } + output +} + +//sub_379CE6 +fn permute_16bit(input: u16, table: &[u8]) -> u16 { + let mut output = 0u16; + for (i, &target_bit) in table.iter().enumerate().take(16) { + if ((1 << i) & input) != 0 { + output |= 1 << target_bit; + } + } + output +} + +//sub_379D0E +fn round_function(input16: u16, key16: u16) -> u16 { + let perm_input = permute_24bit(input16, &P_TABLE_1); + let perm_key = permute_24bit(key16, &P_TABLE_1); + let xor_result = perm_input ^ perm_key; + + let sbox_output = sbox_substitute(xor_result); + + permute_16bit(sbox_output, &P_TABLE_2) +} + +//sub_379DE8 +fn decrypt_block(encrypted32: u32, key_upper: u16, key_lower: u16, prev_block: u32) -> u32 { + let lower_half = (encrypted32 & 0xFFFF) as u16; + let upper_half = ((encrypted32 >> 16) & 0xFFFF) as u16; + + //round 1 + let mut temp1 = round_function(lower_half, key_upper); + temp1 ^= upper_half; + + //round 2 + let mut temp2 = round_function(temp1, key_lower); + temp2 ^= lower_half; + + let decrypted = ((temp2 as u32) << 16) | (temp1 as u32); + + decrypted ^ prev_block +} + +pub fn decrypt(data: &[u8], key: u32, xor_mask: Option) -> Vec { + let key_upper = ((key >> 16) & 0xFFFF) as u16; + let key_lower = (key & 0xFFFF) as u16; + + let mut decrypted = Vec::with_capacity(data.len()); + let mut prev_block = 0u32; //initial iv = 0 + + //calculate how many complete 4 byte blocks + let aligned_len = (data.len() / 4) * 4; + + for chunk in data[..aligned_len].chunks_exact(4) { + let encrypted32 = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + let decrypted32 = decrypt_block(encrypted32, key_upper, key_lower, prev_block); + + decrypted.extend_from_slice(&decrypted32.to_le_bytes()); + prev_block = encrypted32; + } + + //the remaining bytes that are not 4 byte blocks are just XORed with 0x3D + for &byte in &data[aligned_len..] { + decrypted.push(byte ^ 0x3D); + } + + //apply XOR to the first 4 bytes if a mask is provided + if let Some(mask) = xor_mask { + if decrypted.len() >= 4 { + let first_dword = u32::from_le_bytes([ + decrypted[0], + decrypted[1], + decrypted[2], + decrypted[3], + ]); + let xored = first_dword ^ mask; + decrypted[0..4].copy_from_slice(&xored.to_le_bytes()); + } + } + + decrypted +} \ No newline at end of file