PANA_DVD extractor initial added.

This commit is contained in:
theubusu
2025-11-19 23:39:33 +01:00
parent 85a36af3ba
commit da06edf592
10 changed files with 507 additions and 2 deletions
Generated
+7
View File
@@ -697,6 +697,12 @@ dependencies = [
"rand_core", "rand_core",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"
@@ -781,6 +787,7 @@ dependencies = [
"md5", "md5",
"rsa", "rsa",
"sha1", "sha1",
"simd-adler32",
"tar", "tar",
] ]
+2 -1
View File
@@ -16,4 +16,5 @@ rsa = { version = "0.9", features = ["hazmat"] }
hex = "0.4" hex = "0.4"
ecb = "0.1" ecb = "0.1"
tar = "0.4.44" tar = "0.4.44"
binrw = "0.15" binrw = "0.15"
simd-adler32 = "*"
+1
View File
@@ -11,6 +11,7 @@ pub mod roku;
pub mod sony_bdp; pub mod sony_bdp;
pub mod rvp; pub mod rvp;
pub mod funai_upg; pub mod funai_upg;
pub mod pana_dvd;
pub mod pup; pub mod pup;
+266
View File
@@ -0,0 +1,266 @@
use std::fs::File;
use std::path::Path;
use std::fs::{self, OpenOptions};
use std::io::{Write, Read, Cursor, Seek};
use simd_adler32::adler32;
use flate2::read::GzDecoder;
use binrw::{BinRead, BinReaderExt};
use crate::keys;
use crate::common;
use crate::utils::pana_dvd_crypto::{decrypt_data};
use crate::utils::lzss::{decompress_lzss};
#[derive(BinRead)]
struct ModuleEntry {
#[br(count = 4)] name_bytes: Vec<u8>,
#[br(count = 4)] version_bytes: Vec<u8>,
_unk: u32,
offset: u32,
#[br(count = 8)] platform_bytes: Vec<u8>,
_unk1: u16,
#[br(count = 6)] id_bytes: Vec<u8>,
size: u32,
data_checksum: u32,
_unk2: u32,
_entry_checksum: u32,
}
impl ModuleEntry {
fn name(&self) -> String {
common::string_from_bytes(&self.name_bytes)
}
fn version(&self) -> String {
common::string_from_bytes(&self.version_bytes)
}
fn platform(&self) -> String {
common::string_from_bytes(&self.platform_bytes)
}
fn id(&self) -> String {
common::string_from_bytes(&self.id_bytes)
}
fn is_valid(&self) -> bool {
self.name().is_ascii() && self.platform().is_ascii()
}
}
#[derive(BinRead)]
struct MainListHeader {
_checksum: u32,
_unk: u32,
list_size: u32,
_unk2: u64,
}
impl MainListHeader {
fn entry_count(&self) -> u32 {
(&self.list_size - 20) / 8
}
}
#[derive(BinRead)]
struct MainListEntry {
size: u32,
checksum: u32,
}
#[derive(BinRead)]
struct MainEntryHeader {
#[br(count = 14)] _header_string: Vec<u8>, //EXTRHEADDRV
compression_type_byte: u16,
decompressed_size: u32,
_destination_address: u32,
compressed_size: u32,
_unk: u32,
_footer_offset: u32,
_base_address: u32,
_checksum: u32,
_checksum_flag: u8,
#[br(count = 19)] _unused: Vec<u8>,
}
impl MainEntryHeader {
fn compression_type(&self) -> &str {
if self.compression_type_byte == 0 {
return "Uncompressed"
} else if self.compression_type_byte == 1 {
return "GZIP + LZSS"
} else if self.compression_type_byte == 2 {
return "LZSS"
} else {
return "Unknown"
}
}
}
pub fn find_key<'a>(key_array: &'a [&'a str], data: &[u8], expected_magic: &[u8]) -> Result<Option<[u8; 8]>, Box<dyn std::error::Error>> {
for key_hex in key_array {
let key_bytes = hex::decode(key_hex)?;
let key_array: [u8; 8] = match key_bytes.as_slice().try_into() {
Ok(k) => k,
Err(_) => continue,
};
let decrypted = decrypt_data(data, &key_array);
if decrypted.starts_with(expected_magic) {
return Ok(Some(key_array));
}
}
Ok(None)
}
pub fn is_pana_dvd_file(mut file: &File) -> bool {
let header = common::read_file(&file, 0, 16).expect("Failed to read from file.");
if header.starts_with(b"PANASONIC\x00\x00\x00") {
file.seek(std::io::SeekFrom::Start(48)).expect("Failed to seek"); //skip rest of header
true
} else if find_key(&keys::PANA_DVD_KEYONLY, &header, b"PROG").expect("Failed to decrypt header.").is_some() {
true
} else {
false
}
}
fn decompress_gzip(data: &[u8]) -> std::io::Result<Vec<u8>> {
let mut decoder = GzDecoder::new(data);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
}
pub fn extract_pana_dvd(mut file: &File, output_folder: &str) -> Result<(), Box<dyn std::error::Error>> {
let enc_header = common::read_exact(&mut file, 8192)?;
let matching_key;
let header;
// find the key, knowing that the first entry is always "PROG"
if let Some(key_array) = find_key(&keys::PANA_DVD_KEYONLY, &enc_header, b"PROG")? {
println!("Found valid key: {}\n", hex::encode_upper(key_array));
matching_key = Some(key_array);
header = decrypt_data(&enc_header, &key_array);
} else {
println!("No valid key found!\n");
return Ok(());
}
let matching_key_array = matching_key.as_ref().unwrap();
let mut hdr_reader = Cursor::new(header);
let mut modules: Vec<ModuleEntry> = Vec::new();
for i in 0..100 {
let entry: ModuleEntry = hdr_reader.read_le()?;
if !entry.is_valid() {break};
println!("Module {} - Name: {}, Version: {}, Platform: {}, ID: {}, Offset: {}, Size: {}",
i + 1, entry.name(), entry.version(), entry.platform(), entry.id(), entry.offset, entry.size);
if modules.iter().any(|m| m.name() == entry.name()){
println!("- Duplicate module, skipping!");
continue
}
modules.push(entry);
}
let mut main_data: Option<Vec<u8>> = None;
for module in modules {
println!("\nSave module {}, Offset: {}, Size: {}, Expected checksum: {:#010x}",
module.name(), module.offset, module.size, module.data_checksum);
let mut data = common::read_file(&file, module.offset as u64, module.size as usize)?;
let checksum = adler32(&data.as_slice());
println!("Calculated checksum: {:#010x}", checksum);
if checksum == module.data_checksum {
println!("[OK] Checksum correct without decrypt");
} else {
println!("Checksum incorrect, try with decrypt...");
let dec_data = decrypt_data(&data, &matching_key_array);
let checksum = adler32(&dec_data.as_slice());
println!("Calculated checksum: {:#010x}", checksum);
if checksum == module.data_checksum {
println!("[OK] Checksum correct with decrypt");
data = dec_data;
} else {
println!("[NG] Checksum incorrect, unknown data")
}
}
if module.name() == "MAIN" {
main_data = Some(data);
println!("MAIN found - will extract later...");
continue
}
let output_path = Path::new(&output_folder).join(format!("{}.bin", module.name()));
fs::create_dir_all(&output_folder)?;
let mut out_file = OpenOptions::new().write(true).create(true).open(output_path)?;
out_file.write_all(&data)?;
println!("- Saved file!");
}
if !main_data.is_some() {
println!("\nMAIN data not found, Extraction finished!");
return Ok(())
}
println!("\nExtracting MAIN section...");
let main_data_unwrap = main_data.as_ref().unwrap();
let mut main_reader = Cursor::new(main_data_unwrap);
let main_list_hdr: MainListHeader = main_reader.read_le()?;
println!("MAIN entry count: {}", main_list_hdr.entry_count());
let mut main_entries: Vec<MainListEntry> = Vec::new();
for i in 0..main_list_hdr.entry_count() {
let main_entry: MainListEntry = main_reader.read_le()?;
println!("- Entry {}/{} - Size: {}, Checksum: {:#010x}",
i + 1, main_list_hdr.entry_count(), main_entry.size, main_entry.checksum);
main_entries.push(main_entry);
}
for entry in main_entries {
let mut data = common::read_exact(&mut main_reader, entry.size as usize)?;
//decrypt first and last 5kb
let first_decrypted = decrypt_data(&data[..5120], &matching_key_array);
data[..5120].copy_from_slice(&first_decrypted);
let last_decrypted = decrypt_data(&data[entry.size as usize - 5120..], &matching_key_array);
data[entry.size as usize - 5120..].copy_from_slice(&last_decrypted);
//
let mut data_reader = Cursor::new(data);
let header: MainEntryHeader = data_reader.read_le()?;
println!("\nSaving entry - Compressed size: {}, Decompressed size: {}, Compression type: {}({})",
header.compressed_size, header.decompressed_size, header.compression_type_byte, header.compression_type());
let compressed_data = common::read_exact(&mut data_reader, header.compressed_size as usize)?;
let decompressed_data;
if header.compression_type_byte == 1 { //gzip + lzss
println!("- (1/2) Decompressing GZIP...");
let decompressed_gzip = decompress_gzip(&compressed_data)?;
// the decompressed data has another header
let mut decompressed_gzip_reader = Cursor::new(decompressed_gzip);
let header: MainEntryHeader = decompressed_gzip_reader.read_le()?;
println!("- (2/2) Decompressing LZSS... (Compressed size: {}, Decompressed size: {})", header.compressed_size, header.decompressed_size);
let compressed_lzss = common::read_exact(&mut decompressed_gzip_reader, header.compressed_size as usize)?;
decompressed_data = decompress_lzss(&compressed_lzss);
} else if header.compression_type_byte == 2 { //only lzss
println!("- Decompressing LZSS...");
decompressed_data = decompress_lzss(&compressed_data);
} else if header.compression_type_byte == 0 { //no compression. havent encountered one yet
decompressed_data = compressed_data;
} else {
println!("- Unknown compression method!");
decompressed_data = compressed_data;
}
let output_path = Path::new(&output_folder).join("MAIN.bin");
fs::create_dir_all(&output_folder)?;
let mut out_file = OpenOptions::new().append(true).create(true).open(output_path)?;
out_file.write_all(&decompressed_data)?;
println!("- Saved to MAIN!");
}
println!("\nExtraction finished!");
Ok(())
}
+5
View File
@@ -74,4 +74,9 @@ pub static EPK2: &[(&str, &str)] = &[
pub static EPK3: &[(&str, &str)] = &[ pub static EPK3: &[(&str, &str)] = &[
("34CC219D3AFC102433109BBC1DA44095", "m14 (m14tv) - LX webOS 1 (2014)"), ("34CC219D3AFC102433109BBC1DA44095", "m14 (m14tv) - LX webOS 1 (2014)"),
("52A208FA24E7E70730A40999B1C22C148F4920484BC50B515D243E35D14689F1", "o24n - LX webOS 10 (2025)") ("52A208FA24E7E70730A40999B1C22C148F4920484BC50B515D243E35D14689F1", "o24n - LX webOS 10 (2025)")
];
//pana dvd keys (no AES)
pub static PANA_DVD_KEYONLY: &[&str] = &[
("08E03D859AF9F3EE"), //B3A25A0B9D864F08 # 87 devices, ..-~2011
]; ];
+4
View File
@@ -103,6 +103,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("PFL UPG file detected!"); println!("PFL UPG file detected!");
formats::pfl_upg::extract_pfl_upg(&file, &output_path)?; formats::pfl_upg::extract_pfl_upg(&file, &output_path)?;
} }
else if formats::pana_dvd::is_pana_dvd_file(&file) {
println!("PANA_DVD file detected!");
formats::pana_dvd::extract_pana_dvd(&file, &output_path)?;
}
else if formats::pup::is_pup_file(&file) { else if formats::pup::is_pup_file(&file) {
println!("PUP file detected!"); println!("PUP file detected!");
formats::pup::extract_pup(&file, &output_path)?; formats::pup::extract_pup(&file, &output_path)?;
+3 -1
View File
@@ -1 +1,3 @@
pub mod aes; pub mod aes;
pub mod pana_dvd_crypto;
pub mod lzss;
+51
View File
@@ -0,0 +1,51 @@
pub fn decompress_lzss(data: &[u8]) -> Vec<u8> {
let mut window = [0u8; 0x1000];
let mut dst = Vec::new();
let mut src_i = 0;
let mut win_pos = 0xFEE;
let mut flags = 0u16;
while src_i < data.len() {
flags >>= 1;
if (flags & 0x100) == 0 {
if src_i >= data.len() {
break;
}
flags = data[src_i] as u16 | 0xFF00;
src_i += 1;
}
if (flags & 1) == 0 {
// Backreference
if src_i + 1 >= data.len() {
break;
}
let b1 = data[src_i];
let b2 = data[src_i + 1];
src_i += 2;
let mut offset = (b1 as usize) | (((b2 & 0xF0) as usize) << 4);
let length = ((b2 & 0x0F) as usize) + 3;
for _ in 0..length {
let c = window[offset];
dst.push(c);
window[win_pos] = c;
win_pos = (win_pos + 1) & 0xFFF;
offset = (offset + 1) & 0xFFF;
}
} else {
// Literal
if src_i >= data.len() {
break;
}
let c = data[src_i];
src_i += 1;
dst.push(c);
window[win_pos] = c;
win_pos = (win_pos + 1) & 0xFFF;
}
}
dst
}
+167
View File
@@ -0,0 +1,167 @@
//libfmupre.so
//DAT_29f06d60
const SBOX: [u8; 256] = [
0x00,0x01,0xbb,0x9c,0x3f,0x5e,0xc7,0x3e,0x36,0xa4,0x92,0x93,0x38,0x9b,0x8d,0x1a,
0x3c,0x84,0xf7,0x2a,0xcb,0x3d,0x70,0xad,0x30,0xa2,0xc1,0x95,0x03,0x39,0xa5,0x0e,
0xe6,0xfb,0x09,0x1e,0xb9,0xc4,0x24,0x57,0x5f,0xc3,0x5d,0xfa,0x60,0x56,0xfd,0xed,
0x81,0xf3,0x4a,0xff,0x85,0xdb,0xcd,0xa0,0xd0,0x9d,0x8b,0x26,0x4c,0x50,0x0c,0x1d,
0x7d,0x77,0xde,0x7a,0x0a,0x0b,0x73,0xe1,0x54,0x76,0xe8,0xc2,0x28,0x05,0x98,0x99,
0x29,0x4b,0xee,0x87,0x42,0x59,0x65,0xb8,0x1f,0xdf,0x23,0x58,0x63,0x69,0x1c,0x79,
0x64,0xc5,0x6f,0x13,0x3b,0xce,0x08,0xa7,0xb2,0x12,0x20,0x7f,0xe2,0x89,0x21,0x2b,
0x41,0x49,0x7c,0x82,0xbe,0xf2,0x43,0x8e,0x86,0x02,0x9e,0xd8,0x67,0x8f,0xa3,0xe7,
0xbc,0xf8,0x66,0x22,0x4d,0xe5,0xba,0xd2,0xda,0xea,0x61,0x25,0xb0,0xc0,0x7b,0xa6,
0x48,0x96,0xdd,0xdc,0x71,0x90,0x55,0x5b,0x4f,0x4e,0x6d,0xd1,0x11,0x34,0xaa,0x1b,
0xf4,0x17,0x80,0xb7,0xcc,0x8c,0xd9,0x37,0x8a,0x2c,0x94,0x16,0x72,0x40,0xef,0x9a,
0xc8,0x47,0xf6,0xfc,0x2e,0xb3,0x2f,0xbd,0xcf,0xa9,0x15,0x10,0x18,0xec,0x6a,0x6e,
0xc9,0x91,0x53,0x78,0xa8,0x3a,0xaf,0x0d,0xe0,0x97,0x32,0x51,0xb1,0xf5,0x27,0x2d,
0x35,0x88,0x14,0xe3,0xfe,0x06,0xd7,0xd6,0xab,0xa1,0xd5,0xe4,0x45,0x44,0x9f,0x0f,
0x5a,0x83,0xeb,0xca,0x07,0x04,0xb4,0xb6,0x52,0xae,0xd4,0x33,0x31,0x75,0xd3,0x6c,
0x62,0xbf,0x6b,0xf9,0xac,0x46,0xf0,0x5c,0x19,0x7e,0x68,0xb5,0xf1,0x74,0xc6,0xe9,
];
//FUN_29f05520
fn round_function(state: &mut [u8; 4], a5: &[u8; 4]) {
let v5 = a5[1] ^ state[1];
let v6 = a5[2] ^ state[2];
let v7 = state[3] ^ a5[3];
let v8 = ((v5 >> 7) | (v5 << 1)) & 0xFF;
let v9 = ((v6 >> 5) | (v6 << 3)) & 0xFF;
let v10 = a5[0] ^ state[0];
let v11 = ((v7 >> 2) | (v7 << 6)) & 0xFF;
let v12 = v10 ^ v9 ^ v8;
let temp_v7 = v10 >> 1;
let temp_v10 = ((v10 << 7) | (v11 >> 1)) & 0xFF;
let new_a4 = ((v9 >> 1) | (v11 << 7)) & 0xFF;
let v13 = SBOX[(v11 ^ v12) as usize];
let new_a3 = ((v8 >> 1) | (v9 << 7)) & 0xFF;
let new_a2 = (temp_v7 | (v8 << 7)) & 0xFF;
let new_result = temp_v10 & 0xFF;
let a4_final = new_a4.wrapping_add(v13);
let a3_final = new_a3.wrapping_add(((v13 >> 7) | (v13 << 1)) & 0xFF);
let a2_final = new_a2.wrapping_add(((v13 >> 6) | (v13 << 2)) & 0xFF);
let result_final = new_result.wrapping_add(((v13 >> 5) | (v13 << 3)) & 0xFF);
state[0] = result_final;
state[1] = a2_final;
state[2] = a3_final;
state[3] = a4_final;
}
//FUN_29f055dc
fn key_schedule(xored_key: &[u8; 8], out: &mut [u8; 32]) {
//in original function 2 keys were XORed together to make the actual key (that the round keys are derived from)
// why? i dont know (obfuscation?) but i use the XORed key here directly
let mut v26 = [0u8; 8];
for i in 0..7 {
v26[i] = xored_key[i];
}
let v12 = xored_key[7];
v26[1] ^= v12;
v26[4] ^= v12;
for i in 0..7 {
out[i] = v26[i];
}
for i in 7..32 {
out[i] = 0;
}
let mut v22: [u8; 4] = [53, 125, 251, 154];
for v16 in 0..16 {
let v17 = 4 * (v16 & 7);
let mut state: [u8; 4] = out[v17..v17 + 4].try_into().unwrap();
round_function(&mut state, &v22);
out[v17..v17 + 4].copy_from_slice(&state);
v22.copy_from_slice(&state);
}
}
//FUN_29f056b0
fn decrypt_block(a1: &[u8; 32], a2: &mut [u8; 8]) {
let mut v6 = a2[0];
let mut v7 = a2[1];
let mut v5 = a2[2];
let mut v4 = a2[3];
let mut v24 = v6;
let mut v25 = v7;
let mut v26 = v5;
let mut v27 = v4;
let mut v10 = a2[4];
let mut v11 = a2[5];
let mut v12 = a2[6];
let mut v13 = a2[7];
let mut v15: u8 = 0;
let mut v16: u8 = 0;
let mut v17: u8 = 0;
let mut v18: u8 = 0;
for v2 in (0..16).rev() {
let v14 = (v2 & 7) as usize;
let subkey: [u8; 4] = a1[4 * v14..4 * v14 + 4].try_into().unwrap();
let mut st = [v24, v25, v26, v27];
round_function(&mut st, &subkey);
v24 = st[0];
v25 = st[1];
v26 = st[2];
v27 = st[3];
v15 = v10 ^ v24;
v16 = v11 ^ v25;
v17 = v12 ^ v26;
v18 = v13 ^ v27;
v24 ^= v10;
v25 ^= v11;
v26 ^= v12;
v27 ^= v13;
v10 = v6;
v11 = v7;
v12 = v5;
v13 = v4;
if v2 == 0 {
break;
}
v6 = v15;
v7 = v16;
v5 = v17;
v4 = v18;
}
a2[0] = v6;
a2[1] = v7;
a2[2] = v5;
a2[3] = v4;
a2[4] = v15;
a2[5] = v16;
a2[6] = v17;
a2[7] = v18;
}
fn decrypt_data_inplace(data: &mut [u8], key: &[u8; 8]) {
let mut subkeys = [0u8; 32];
key_schedule(key, &mut subkeys);
for chunk in data.chunks_mut(8) {
if chunk.len() == 8 {
let mut block: [u8; 8] = chunk.try_into().unwrap();
decrypt_block(&subkeys, &mut block);
chunk.copy_from_slice(&block);
}
}
}
pub fn decrypt_data(data: &[u8], key: &[u8; 8]) -> Vec<u8> {
let mut buf = data.to_vec();
decrypt_data_inplace(&mut buf, key);
buf
}
+1
View File
@@ -12,6 +12,7 @@
| Mediatek PKG | Many Mediatek-based TVs (Hisense, Sony, Panasonic, Philips...) | Only files with 144 byte header and using 4xVendor magic as enc key are supported | https://github.com/openlgtv/epk2extract | | Mediatek PKG | Many Mediatek-based TVs (Hisense, Sony, Panasonic, Philips...) | Only files with 144 byte header and using 4xVendor magic as enc key are supported | https://github.com/openlgtv/epk2extract |
| Mediatek upgrade_loader | Older Mediatek-based TVs | All files should be supported providing they are not encrypted | - | | Mediatek upgrade_loader | Older Mediatek-based TVs | All files should be supported providing they are not encrypted | - |
| Novatek pkg | Some Novatek-based TVs (Philips, LG..) | All files should be supported | https://github.com/openlgtv/epk2extract | | Novatek pkg | Some Novatek-based TVs (Philips, LG..) | All files should be supported | https://github.com/openlgtv/epk2extract |
| Panasonic Blu-Ray (PANA_DVD) | Panasonic Blu-Ray Players/Recorders | Only some files are supported (**depends on keys**) | - |
| Philips UPG | Philips pre-TPVision TVS 2008-2013 | Only some files are supported (**depends on keys**) | https://github.com/frederic/pflupg-tool | | Philips UPG | Philips pre-TPVision TVS 2008-2013 | Only some files are supported (**depends on keys**) | https://github.com/frederic/pflupg-tool |
| PUP | Sony PlayStation 4/ PlayStation 5 | File has to be decrypted | https://github.com/Zer0xFF/ps4-pup-unpacker | | PUP | Sony PlayStation 4/ PlayStation 5 | File has to be decrypted | https://github.com/Zer0xFF/ps4-pup-unpacker |
| RUF | Samsung Broadcom-based Blu-Ray players | Only BD-P1600 is supported (**depends on keys**) | - | | RUF | Samsung Broadcom-based Blu-Ray players | Only BD-P1600 is supported (**depends on keys**) | - |