+ added vestel

This commit is contained in:
Pari Malam
2026-06-04 19:46:23 +08:00
parent 6c51de5d9c
commit 7ccf2519ab
36 changed files with 810 additions and 222 deletions
Generated
+184 -5
View File
@@ -19,6 +19,15 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.20"
@@ -26,7 +35,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-parse 0.2.7",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse 1.0.0",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
@@ -36,9 +60,9 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.11"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
@@ -49,6 +73,15 @@ dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.4"
@@ -221,7 +254,7 @@ version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
dependencies = [
"anstream",
"anstream 0.6.20",
"anstyle",
"clap_lex",
"strsim",
@@ -346,6 +379,29 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "env_filter"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
dependencies = [
"anstream 1.0.0",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "errno"
version = "0.3.14"
@@ -454,6 +510,30 @@ dependencies = [
"either",
]
[[package]]
name = "jiff"
version = "0.2.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde_core",
]
[[package]]
name = "jiff-static"
version = "0.2.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "jobserver"
version = "0.1.34"
@@ -528,6 +608,12 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "log"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f"
[[package]]
name = "lz4"
version = "1.28.1"
@@ -563,6 +649,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
[[package]]
name = "memchr"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "minilzo-rs"
version = "0.6.1"
@@ -676,6 +768,21 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "portable-atomic-util"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
dependencies = [
"portable-atomic",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -779,6 +886,35 @@ dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rsa"
version = "0.9.10"
@@ -812,6 +948,26 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sha1"
version = "0.10.6"
@@ -901,6 +1057,26 @@ dependencies = [
"xattr",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "typenum"
version = "1.20.0"
@@ -915,7 +1091,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "unixtract"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"aes",
"binrw",
@@ -924,9 +1100,11 @@ dependencies = [
"clap",
"des",
"ecb",
"env_logger",
"flate2",
"hex",
"liblzma",
"log",
"lz4",
"lzma-rs",
"md5",
@@ -937,6 +1115,7 @@ dependencies = [
"sha1",
"simd-adler32",
"tar",
"thiserror",
"zstd",
]
+8 -2
View File
@@ -1,7 +1,10 @@
[package]
name = "unixtract"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
description = "Firmware extractor for various file formats from TVs and AV devices"
license = "GPL-3.0"
repository = "https://github.com/theubusu/unixtract"
[dependencies]
clap = { version = "4.5", features = ["derive"] }
@@ -24,4 +27,7 @@ prost-types = "0.14.1"
bzip2 = "0.6.1"
liblzma = "0.4.5"
des = "0.8.1"
zstd = "0.13.3"
zstd = "0.13.3"
thiserror = "2.0"
log = "0.4"
env_logger = "0.11"
+79
View File
@@ -0,0 +1,79 @@
//! Unified error type for unixtract.
//!
//! All fallible operations in unixtract return [`UnixtractError`] instead of
//! panicking with `.unwrap()` or `.expect()`. This module provides a single,
//! well-structured error enum that covers all failure modes encountered during
//! firmware format detection, parsing, decryption, decompression, and
//! extraction.
use std::path::PathBuf;
/// The top-level error type used throughout unixtract.
///
/// Variants are defined for all anticipated error categories. Some may not
/// yet be used in every format module but are available for gradual adoption.
#[derive(Debug, thiserror::Error)]
#[allow(dead_code)]
pub enum UnixtractError {
/// An I/O error occurred (file not found, permission denied, etc.).
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
/// A file format was detected but the header contents are invalid.
#[error("Invalid format: {0}")]
InvalidFormat(String),
/// The input file's format could not be recognized by any detector.
#[error("Unrecognized input format")]
UnrecognizedFormat,
/// A cryptographic operation (AES, DES, RSA, etc.) failed.
#[error("Crypto error: {0}")]
Crypto(String),
/// A decompression operation (zlib, lzma, lz4, etc.) failed.
#[error("Decompression error: {0}")]
Decompression(String),
/// A required key was not found in the key store.
#[error("Key not found: {0}")]
KeyNotFound(String),
/// A checksum or integrity check failed.
#[error("Checksum mismatch: expected {expected:#010x}, got {actual:#010x}")]
ChecksumMismatch { expected: u32, actual: u32 },
/// A path extracted from a firmware image is unsafe (e.g., contains `..`).
#[error("Unsafe path in firmware image: {path}")]
UnsafePath { path: PathBuf },
/// The format was detected but the specific variant/version is unsupported.
#[error("Unsupported variant: {0}")]
UnsupportedVariant(String),
/// A generic error that doesn't fit the more specific variants.
#[error("{0}")]
Other(String),
}
/// Convenience type alias used across the crate.
#[allow(dead_code)]
pub type Result<T> = std::result::Result<T, UnixtractError>;
impl From<String> for UnixtractError {
fn from(s: String) -> Self {
UnixtractError::Other(s)
}
}
impl From<&str> for UnixtractError {
fn from(s: &str) -> Self {
UnixtractError::Other(s.to_string())
}
}
impl From<binrw::Error> for UnixtractError {
fn from(e: binrw::Error) -> Self {
UnixtractError::InvalidFormat(format!("Binary parse error: {e}"))
}
}
+30 -5
View File
@@ -1,6 +1,23 @@
//! Format registry and detection/extraction framework.
//!
//! Each supported firmware format has a detector function (`is_*_file`) that
//! checks whether a given input matches that format, and an extractor function
//! (`extract_*`) that performs the actual extraction. The [`Format`] struct
//! pairs these together, and [`get_registry`] returns the ordered list of all
//! supported formats.
//!
//! **Important**: The order of formats in the registry matters. Some formats
//! can contain other formats as inner payloads, so the outer format must be
//! detected first. Ordering constraints are noted in comments.
use std::any::Any;
use crate::AppContext;
/// A supported firmware format with its detector and extractor functions.
///
/// The `detector_func` returns `Some(ctx)` if the input matches this format,
/// where `ctx` is an opaque box of format-specific context data that will be
/// passed to the `extractor_func`. Returns `None` if the format doesn't match.
pub struct Format {
pub name: &'static str,
pub detector_func: fn(&AppContext) -> Result<Option<Box<dyn Any>>, Box<dyn std::error::Error>>,
@@ -35,10 +52,10 @@ pub mod gx_dvb;
pub mod onkyo;
pub mod philips_bdp;
pub mod tsb_bin;
pub mod pup;
pub mod vestel;
pub mod pup;
pub mod msd;
pub mod msd;
pub mod msd10;
pub mod msd11;
@@ -53,7 +70,11 @@ pub mod mtk_pkg_old;
pub mod mtk_pkg_new;
pub mod mtk_bdp;
//define all formats here
/// Returns the ordered list of all supported firmware formats.
///
/// Formats are tried in order during detection; the first match wins. This
/// means that container formats (which embed other formats) must appear
/// before their inner formats to avoid misdetection.
pub fn get_registry() -> Vec<Format> {
return vec![
Format {
@@ -256,5 +277,9 @@ pub fn get_registry() -> Vec<Format> {
detector_func: crate::formats::tsb_bin::is_tsb_bin_file,
extractor_func: crate::formats::tsb_bin::extract_tsb_bin,
},
]
Format {
name: "vestel",
detector_func: crate::formats::vestel::is_vestel_file,
extractor_func: crate::formats::vestel::extract_vestel,
}, ]
}
-1
View File
@@ -33,7 +33,6 @@ pub fn extract_bdl(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> Result<(), Box<d
for _i in 0..header.pkg_count {
let pkg_entry: PkgListEntry = file.read_le()?;
//println!("Package {} - Offset: {}, Size: {}", i + 1, pkg_entry.offset, pkg_entry.size);
pkgs.push(pkg_entry);
}
+12 -38
View File
@@ -3,6 +3,7 @@ use std::io::Seek;
use crate::AppContext;
use crate::utils::common;
use crate::utils::aes::decrypt_aes_ecb_auto;
use crate::formats;
pub struct EpkContext {
@@ -29,13 +30,13 @@ fn check_epk_version(versions: &[u8]) -> Option<u8> {
if match_with_pattern(&versions, epk2_pattern) {
Some(2)
} else if match_with_pattern(&versions, epk3_pattern) {
} else if match_with_pattern(&versions, epk3_pattern) {
Some(3)
} else if match_with_pattern(&versions, epk3_another_pattern) {
} else if match_with_pattern(&versions, epk3_another_pattern) {
Some(3)
} else if match_with_pattern(&versions, epk3_new_pattern) {
} else if match_with_pattern(&versions, epk3_new_pattern) {
Some(3)
}else {
} else {
None
}
}
@@ -61,7 +62,7 @@ pub fn extract_epk(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<(), Box<dy
let platform_version = common::string_from_bytes(&versions[4..20]);
let sdk_version = common::string_from_bytes(&versions[20..36]);
println!("Platform version: {}\nSDK version: {}", platform_version, sdk_version);
file.seek(std::io::SeekFrom::Start(0))?;
if ctx.epk_version == 2 {
@@ -75,7 +76,11 @@ pub fn extract_epk(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<(), Box<dy
Ok(())
}
//COMMON EPK FUNCTIONS
// COMMON EPK FUNCTIONS
/// Try each key in `key_array` to find one that decrypts `data` with a header
/// matching `expected_magic`. Returns the key name and the raw key bytes on
/// success, or `None` if no key works.
pub fn find_key<'a>(key_array: &'a [(&'a str, &'a str)], data: &[u8], expected_magic: &[u8]) -> Result<Option<(&'a str, Vec<u8>)>, Box<dyn std::error::Error>> {
for (key_hex, name) in key_array {
let key_bytes = hex::decode(key_hex)?;
@@ -83,41 +88,10 @@ pub fn find_key<'a>(key_array: &'a [(&'a str, &'a str)], data: &[u8], expected_m
Ok(d) => d,
Err(_) => continue,
};
if decrypted.starts_with(expected_magic) {
return Ok(Some((name, key_bytes)));
}
}
Ok(None)
}
use aes::Aes128;
use aes::Aes256;
use ecb::{Decryptor, cipher::{BlockDecryptMut, KeyInit, generic_array::GenericArray}};
type Aes128EcbDec = Decryptor<Aes128>;
type Aes256EcbDec = Decryptor<Aes256>;
pub fn decrypt_aes_ecb_auto(key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut buffer = ciphertext.to_vec();
if key.len() == 32 {
// aes256
let key_array: [u8; 32] = key.try_into()?;
let mut decryptor = Aes256EcbDec::new(&key_array.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));
}
} else {
// aes128
let key_array: [u8; 16] = key.try_into()?;
let mut decryptor = Aes128EcbDec::new(&key_array.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)
}
+2 -1
View File
@@ -10,7 +10,8 @@ use binrw::BinReaderExt;
use crate::utils::common;
use crate::utils::global::opt_dump_dec_hdr;
use crate::keys;
use crate::formats::epk::{decrypt_aes_ecb_auto, find_key};
use crate::formats::epk::find_key;
use crate::utils::aes::decrypt_aes_ecb_auto;
use include::*;
pub fn is_epk2_file(app_ctx: &AppContext) -> Result<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
+2 -1
View File
@@ -10,7 +10,8 @@ use binrw::BinReaderExt;
use crate::utils::common;
use crate::utils::global::opt_dump_dec_hdr;
use crate::keys;
use crate::formats::epk::{decrypt_aes_ecb_auto, find_key};
use crate::formats::epk::find_key;
use crate::utils::aes::decrypt_aes_ecb_auto;
use include::*;
pub fn is_epk3_file(_app_ctx: &AppContext) -> Result<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
+6 -4
View File
@@ -67,10 +67,12 @@ pub fn extract_funai_mstar(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<()
//extract SoC which (should be) mstar_secure_old, this is just a simple container for that format ( so we will go funai_mstar -> mstar_secure_old -> mstar (DUMB?) )
if name == "SoC" {
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.join("SoC"),
options: app_ctx.options.clone()
let in_ctx: AppContext = AppContext {
input: InputTarget::File(r_out_file),
output_dir: app_ctx.output_dir.join("SoC"),
options: app_ctx.options.clone(),
dry_run: app_ctx.dry_run,
quiet: app_ctx.quiet,
};
//do check and extarct
+37 -44
View File
@@ -1,69 +1,62 @@
//MSD OUITH parsers
// MSD OUITH parsers
pub mod msd_ouith_parser_old;
pub mod msd_ouith_parser_tizen_1_8;
pub mod msd_ouith_parser_tizen_1_9;
// COMMON MSD FUNCTIONS
use aes::Aes128;
use cbc::{Decryptor, cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit}};
type Aes128CbcDec = Decryptor<Aes128>;
use crate::utils::aes::decrypt_aes128_cbc_pcks7;
/// Decrypt "Salted__" format data using OpenSSL-compatible key derivation
/// (MD5 of passphrase+salt for key, MD5 of key+passphrase+salt for IV).
pub fn decrypt_aes_salted_old(encrypted_data: &[u8], passphrase_bytes: &Vec<u8>) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut data = encrypted_data.to_vec();
if data[0..8].to_vec() != b"Salted__" {
if encrypted_data.len() < 16 || encrypted_data[0..8].to_vec() != b"Salted__" {
return Err("Invalid encrypted data!".into());
}
let salt = &data[8..16];
let salt = &encrypted_data[8..16];
//key = md5 of (passphrase + salt)
let mut key = Vec::new();
key.extend_from_slice(&passphrase_bytes);
key.extend_from_slice(&salt);
let key_md5 = md5::compute(&key);
// key = MD5(passphrase + salt)
let mut key_input = Vec::new();
key_input.extend_from_slice(&passphrase_bytes);
key_input.extend_from_slice(&salt);
let key_md5 = md5::compute(&key_input);
//iv = md5 of (md5 of key + passphrase + salt)
let mut iv = Vec::new();
iv.extend_from_slice(&key_md5.0);
iv.extend_from_slice(&passphrase_bytes);
iv.extend_from_slice(&salt);
let iv_md5 = md5::compute(&iv);
// iv = MD5(key + passphrase + salt)
let mut iv_input = Vec::new();
iv_input.extend_from_slice(&key_md5.0);
iv_input.extend_from_slice(&passphrase_bytes);
iv_input.extend_from_slice(&salt);
let iv_md5 = md5::compute(&iv_input);
let decryptor = Aes128CbcDec::new((&key_md5.0).into(), (&iv_md5.0).into());
let decrypted = decryptor.decrypt_padded_mut::<Pkcs7>(&mut data[16..])
.map_err(|e| format!("Decryption error!!: {:?}", e))?;
Ok(decrypted.to_vec())
let key: [u8; 16] = key_md5.0;
let iv: [u8; 16] = iv_md5.0;
decrypt_aes128_cbc_pcks7(&encrypted_data[16..], &key, &iv)
}
/// Decrypt "Salted__" format data using Tizen-style key derivation
/// (passphrase used directly as key, MD5 of salt for IV).
pub fn decrypt_aes_salted_tizen(encrypted_data: &[u8], passphrase_bytes: &Vec<u8>) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut data = encrypted_data.to_vec();
if data[0..8].to_vec() != b"Salted__" {
if encrypted_data.len() < 16 || encrypted_data[0..8].to_vec() != b"Salted__" {
return Err("Invalid encrypted data!".into());
}
let salt = &data[8..16];
let salt = &encrypted_data[8..16];
//iv = md5 of salt
// iv = MD5(salt)
let iv = md5::compute(&salt);
let key: [u8; 16] = passphrase_bytes.as_slice().try_into()
.map_err(|_| "Passphrase must be 16 bytes for AES-128")?;
let iv: [u8; 16] = iv.0;
let decryptor = Aes128CbcDec::new((&(**passphrase_bytes)).into(), (&iv.0).into());
let decrypted = decryptor.decrypt_padded_mut::<Pkcs7>(&mut data[16..])
.map_err(|e| format!("Decryption error!!: {:?}", e))?;
Ok(decrypted.to_vec())
decrypt_aes128_cbc_pcks7(&encrypted_data[16..], &key, &iv)
}
/// Decrypt data using Tizen-style AES-128-CBC with passphrase as key and
/// MD5 of salt as IV.
pub fn decrypt_aes_tizen(encrypted_data: &[u8], passphrase_bytes: &Vec<u8>, salt: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut data = encrypted_data.to_vec();
//iv = md5 of salt
// iv = MD5(salt)
let iv = md5::compute(&salt);
let key: [u8; 16] = passphrase_bytes.as_slice().try_into()
.map_err(|_| "Passphrase must be 16 bytes for AES-128")?;
let iv: [u8; 16] = iv.0;
let decryptor = Aes128CbcDec::new((&(**passphrase_bytes)).into(), (&iv.0).into());
let decrypted = decryptor.decrypt_padded_mut::<Pkcs7>(&mut data)
.map_err(|e| format!("Decryption error!!: {:?}", e))?;
Ok(decrypted.to_vec())
}
decrypt_aes128_cbc_pcks7(&encrypted_data, &key, &iv)
}
+2 -2
View File
@@ -125,7 +125,7 @@ pub fn parse_ouith_blob(blob: &[u8], print_tree: bool) -> Result<(Vec<MSDItem>,
if print_tree { println!(" Type descriptor(0x{:02x}) - Size: {}", type_descriptor.tag, type_descriptor.size); };
let destination_info: CommonDestinationInfo = reader.read_be()?;
if print_tree {
println!(" Name lenght: {}", destination_info.name_len);
println!(" Name length: {}", destination_info.name_len);
println!(" Name: {}", destination_info.name());
println!(" Version: {}", destination_info.version);
};
@@ -236,7 +236,7 @@ pub fn parse_ouith_blob(blob: &[u8], print_tree: bool) -> Result<(Vec<MSDItem>,
if print_tree { println!(" OUSWImageVersionExDesc(0x12) - Size: {}", version_descriptor.size); };
let sw_image_version_ex_desc: OUSWImageVersionExDesc = reader.read_be()?;
if print_tree {
println!(" Name lenght: {}", sw_image_version_ex_desc.name_len);
println!(" Name length: {}", sw_image_version_ex_desc.name_len);
println!(" Name: {}", sw_image_version_ex_desc.name());
println!(" Major version: {}", sw_image_version_ex_desc.major_ver);
println!(" Minor version: {}", sw_image_version_ex_desc.minor_ver);
@@ -146,7 +146,7 @@ pub fn parse_blob_1_8(blob: &[u8], print_tree: bool) -> Result<(Vec<MSDItem>, Op
if print_tree { println!(" OUPartitionVersionDesc(0x0A) - Size: {}", sub_descriptor.size); };
let partition_version_desc: OUPartitionVersionDesc = reader.read_be()?;
if print_tree {
println!(" Name lenght: {}", partition_version_desc.name_len);
println!(" Name length: {}", partition_version_desc.name_len);
println!(" Name: {}", partition_version_desc.name());
println!(" Version: {}", partition_version_desc.version);
};
@@ -240,7 +240,7 @@ pub fn parse_blob_1_8(blob: &[u8], print_tree: bool) -> Result<(Vec<MSDItem>, Op
if print_tree { println!(" OUSWImageVersionDesc(0x19) - Size: {}", sub_descriptor.size); };
let sw_image_version_desc: OUSWImageVersionDesc = reader.read_be()?;
if print_tree {
println!(" Name lenght: {}", sw_image_version_desc.name_len);
println!(" Name length: {}", sw_image_version_desc.name_len);
println!(" Name: {}", sw_image_version_desc.name());
println!(" Major ver: {}", sw_image_version_desc.major_ver);
println!(" Minor ver: {}", sw_image_version_desc.minor_ver);
@@ -151,7 +151,7 @@ pub fn parse_blob_1_9(blob: &[u8], print_tree: bool) -> Result<(Vec<MSDItem>, Op
if print_tree { println!(" OUPartitionVersionDesc(0x0A) - Size: {}", sub_descriptor.size); };
let partition_version_desc: OUPartitionVersionDesc = reader.read_le()?;
if print_tree {
println!(" Name lenght: {}", partition_version_desc.name_len);
println!(" Name length: {}", partition_version_desc.name_len);
println!(" Name: {}", partition_version_desc.name());
};
@@ -240,7 +240,7 @@ pub fn parse_blob_1_9(blob: &[u8], print_tree: bool) -> Result<(Vec<MSDItem>, Op
if print_tree { println!(" OUSWImageVersionDesc(0x19) - Size: {}", sub_descriptor.size); };
let sw_image_version_desc: OUSWImageVersionDesc = reader.read_le()?;
if print_tree {
println!(" Name lenght: {}", sw_image_version_desc.name_len);
println!(" Name length: {}", sw_image_version_desc.name_len);
println!(" Name: {}", sw_image_version_desc.name());
println!(" Major ver: {}", sw_image_version_desc.major_ver);
println!(" Minor ver: {}", sw_image_version_desc.minor_ver);
+1 -2
View File
@@ -35,7 +35,6 @@ pub fn extract_mstar(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> Result<(), Box
}
let mut script_string = String::from_utf8_lossy(&script);
//println!("{}", script_string);
if script_string == "" {
//try for hisense
println!("Failed to get script at 0x0, trying 0x1000...");
@@ -153,7 +152,7 @@ pub fn extract_mstar(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> Result<(), Box
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..");
println!("- Decompressing LZO..");
unlzop_to_file(&data, output_path)?;
println!("-- Saved file!");
i += 1;
+6 -4
View File
@@ -51,10 +51,12 @@ pub fn extract_mstar_secure_old(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Resu
//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()
let in_ctx: AppContext = AppContext {
input: InputTarget::File(r_out_file),
output_dir: app_ctx.output_dir.clone(),
options: app_ctx.options.clone(),
dry_run: app_ctx.dry_run,
quiet: app_ctx.quiet,
};
//do check just in case and extract
+3 -3
View File
@@ -10,7 +10,7 @@ pub static PITIT_END_MARKER: u32 = 0x69_54_49_50; //PITi - end marker of PITIT
#[derive(BinRead)]
pub struct PITITPITEntry {
pub nand_size: u32,
pub nand_size: u32,
pub pit_offset: u32,
pub pit_size: u32,
_table_id: u32,
@@ -18,7 +18,7 @@ pub struct PITITPITEntry {
#[derive(BinRead)]
pub struct PITITBITEntry {
pub bit_offset: u32,
pub bit_offset: u32,
pub bit_size: u32,
_private_data_1: u32,
_private_data_2: u32,
@@ -29,7 +29,7 @@ pub struct PITHeader {
pub pit_magic: [u8; 8],
_version: u32,
pub first_entry_offset: u32, //"header len"
pub entry_size: u32, //"item lenght"
pub entry_size: u32, //"item length"
pub entry_count: u32, //"item num"
}
+11 -13
View File
@@ -213,12 +213,11 @@ fn unhuff(data: &[u8]) -> Vec<u8> {
let found_j: i32;
loop {
if !get_byte(&mut ctx, &mut in_cur) {
//println!("retA");
//flush
if ctx.code_buf_ptr > 1 { for j in 0..ctx.code_buf_ptr { out.push(ctx.code_buf[j]); } };
return out;
}
if !get_byte(&mut ctx, &mut in_cur) {
// flush
if ctx.code_buf_ptr > 1 { for j in 0..ctx.code_buf_ptr { out.push(ctx.code_buf[j]); } };
return out;
}
if ctx.len < 2 { continue; }
let keyp = (((ctx.len & 0x7) as usize) << 6) | ((ctx.code & 0x3F) as usize);
let mut jdx = lookup_charpos[keyp];
@@ -235,16 +234,15 @@ fn unhuff(data: &[u8]) -> Vec<u8> {
found_j = jdx as i32;
let b = ((jdx as i32) >> 1) as u8;
if ctx.code_buf_ptr < ctx.code_buf.len() { ctx.code_buf[ctx.code_buf_ptr] = b; ctx.code_buf_ptr += 1; }
break;
break;
}
ctx.code = 0;
for _ in 0..7 { if !get_byte(&mut ctx, &mut in_cur) {
//println!("retB");
//flush
if ctx.code_buf_ptr > 1 { for j in 0..ctx.code_buf_ptr { out.push(ctx.code_buf[j]); } };
return out;
}
}
// flush
if ctx.code_buf_ptr > 1 { for j in 0..ctx.code_buf_ptr { out.push(ctx.code_buf[j]); } };
return out;
}
}
let combined = (ctx.code | ((found_j as u32) << 7)) as u32;
if ctx.code_buf_ptr < ctx.code_buf.len() { ctx.code_buf[ctx.code_buf_ptr] = (combined & 0xFF) as u8; ctx.code_buf_ptr += 1; }
ctx.code = 0; ctx.len = 0;
+17 -33
View File
@@ -1,14 +1,13 @@
use crate::utils::common;
use crate::utils::aes::decrypt_aes256_ecb;
use binrw::BinRead;
use aes::Aes256;
use ecb::{Decryptor, cipher::{BlockDecryptMut, KeyInit, generic_array::GenericArray}};
use crate::keys;
use rsa::{RsaPublicKey, BigUint};
pub fn try_find_key(sig: &[u8], ciphertext: &[u8]) -> Result<Option<(String, [u8; 32])>, Box<dyn std::error::Error>> {
let mut result: Option<(String, [u8; 32])> = None;
for (name, n_hex) in keys::PFLUPG {
for (name, n_hex) in keys::PFLUPG {
let n = BigUint::from_bytes_be(&hex::decode(n_hex)?);
let e = BigUint::from_bytes_be(&hex::decode("010001")?);
let pubkey = RsaPublicKey::new(n, e)?;
@@ -17,17 +16,17 @@ pub fn try_find_key(sig: &[u8], ciphertext: &[u8]) -> Result<Option<(String, [u8
let dec_int = rsa::hazmat::rsa_encrypt(&pubkey, &sig_int)?;
let dec_sig = dec_int.to_bytes_le();
let aes_key = &dec_sig[20..52];
let dec_ciphertext = decrypt_aes256_ecb(aes_key.try_into().unwrap(), &ciphertext)?;
//needs to start with null-termninated filename string
let aes_key: [u8; 32] = dec_sig[20..52].try_into().unwrap();
let dec_ciphertext = decrypt_aes256_ecb(&aes_key, &ciphertext)?;
// Needs to start with null-terminated filename string
let end = match dec_ciphertext.iter().position(|&b| b == 0) {
Some(pos) => pos,
None => continue, //there is no 0, continue
None => continue, // There is no 0, continue
};
let fname = &dec_ciphertext[..end];
if fname.len() > 1 && fname.is_ascii() { //is ascii filename
result = Some((name.to_string(), aes_key.try_into().unwrap()));
if fname.len() > 1 && fname.is_ascii() { // Is ASCII filename
result = Some((name.to_string(), aes_key));
break
}
}
@@ -35,31 +34,16 @@ pub fn try_find_key(sig: &[u8], ciphertext: &[u8]) -> Result<Option<(String, [u8
Ok(result)
}
type Aes256EcbDec = Decryptor<Aes256>;
pub fn decrypt_aes256_ecb(key: [u8; 32], ciphertext: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut decryptor = Aes256EcbDec::new(&key.into());
let mut buffer = ciphertext.to_vec();
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)
}
#[derive(BinRead)]
pub struct Header {
_magic_bytes: [u8; 8],
pub header_size: u32, //data start
pub data_size: u32,
_crc32: u32,
pub mask: u32,
_data_size_decompressed: u32,
_padding2: u32,
description_bytes: [u8; 512],
_crc32: u32,
pub mask: u32,
_data_size_decompressed: u32,
_padding2: u32,
description_bytes: [u8; 512],
}
impl Header {
pub fn description(&self) -> String {
@@ -74,8 +58,8 @@ impl Header {
pub struct FileHeader {
file_name_bytes: [u8; 60],
pub real_size: u32,
pub stored_size: u32,
pub header_size: u32,
pub stored_size: u32,
pub header_size: u32,
pub attributes: [u8; 4],
}
impl FileHeader {
@@ -91,4 +75,4 @@ impl FileHeader {
pub fn is_package(&self) -> bool {
(self.attributes[3] & (1 << 2)) != 0
}
}
}
+8 -5
View File
@@ -8,6 +8,7 @@ use std::fs::{self, File, OpenOptions};
use binrw::BinReaderExt;
use crate::utils::common;
use crate::utils::aes::decrypt_aes256_ecb;
use include::*;
pub fn is_pfl_upg_file(app_ctx: &AppContext) -> Result<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
@@ -58,7 +59,7 @@ pub fn extract_pfl_upg(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> Result<(), B
let encrypted_data = common::read_exact(&mut file, (header.data_size as usize + 0xf) & !0xf)?;
println!("Decrypting data...");
data = decrypt_aes256_ecb(aes_key, &encrypted_data)?;
data = decrypt_aes256_ecb(&aes_key, &encrypted_data)?;
data.truncate(header.data_size as usize); //discard padding
} else {
@@ -108,10 +109,12 @@ pub fn extract_pfl_upg(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> Result<(), B
//REOPEN temp file and make ctx
let r_temp_file = File::open(&temp_path)?;
let in_ctx: AppContext = AppContext {
input: InputTarget::File(r_temp_file),
output_dir: output_path,
options: app_ctx.options.clone()
let in_ctx: AppContext = AppContext {
input: InputTarget::File(r_temp_file),
output_dir: output_path,
options: app_ctx.options.clone(),
dry_run: app_ctx.dry_run,
quiet: app_ctx.quiet,
};
//do check just in case and extract
+1 -2
View File
@@ -74,8 +74,7 @@ pub fn extract_philips_bdp(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<()
println!("Checking if it's also MTK BDP...");
let new_file = File::open(&output_path)?;
//DUMB
let mtk_ctx: AppContext = AppContext { input: InputTarget::File(new_file), output_dir: app_ctx.output_dir.join("0"), options: app_ctx.options.clone() };
let mtk_ctx: AppContext = AppContext { input: InputTarget::File(new_file), output_dir: app_ctx.output_dir.join("0"), options: app_ctx.options.clone(), dry_run: app_ctx.dry_run, quiet: app_ctx.quiet };
if let Some(result) = formats::mtk_bdp::is_mtk_bdp_file(&mtk_ctx)? {
println!("- MTK BDP file detected!\n");
+2 -2
View File
@@ -19,8 +19,8 @@ pub struct AImageHeader {
_release_id: u32, // toplevel build / release version
_platform_id: u32, // target platform information
pub image_type: u32, // image type
pub lenght: u32, // length of entire image (header + data + trailing padding)
_data_lenght: u32, // length of image data (ie from data_start_offset to start of trailing padding)
pub length: u32, // length of entire image (header + data + trailing padding)
_data_length: u32, // length of image data (ie from data_start_offset to start of trailing padding)
pub data_start_offset: u32, // 0 if header is part of data, sizeof(aimage_v1_header_t) if header is prepended to data
_data_link_address: u32, // for non-pic executable images, where should the image placed in memory in order to execute
_data_entry_point_offset: u32, // 0 if execution entry point is at the beginning of image data
+6 -7
View File
@@ -60,18 +60,17 @@ pub fn extract_roku(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> Result<(), Box<
while image_reader.stream_position()? < size as u64 {
let image: AImageHeader = image_reader.read_le()?;
println!(" #{} - Type: {}(0x{:x}), Lenght: {}, encmode: {}",
i, image.image_type_str(), image.image_type, image.lenght, image.encmode_str());
//println!("{:?}", image);
let data =
println!(" #{} - Type: {}(0x{:x}), Length: {}, encmode: {}",
i, image.image_type_str(), image.image_type, image.length, image.encmode_str());
let data =
if image.data_start_offset == 0 { // "0 if header is part of data"
image_reader.seek(SeekFrom::Current(-256))?; //rewind header
common::read_exact(&mut image_reader, image.lenght as usize)?
common::read_exact(&mut image_reader, image.length as usize)?
} else {
let _skip_data = common::read_exact(&mut image_reader, image.data_start_offset as usize - 256)?;
common::read_exact(&mut image_reader, image.lenght as usize - image.data_start_offset as usize)?
common::read_exact(&mut image_reader, image.length as usize - image.data_start_offset as usize)?
};
let folder_path = Path::new(&app_ctx.output_dir).join(&path);
+1 -3
View File
@@ -100,10 +100,8 @@ pub fn extract_samsung_old(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> Result<(
iv.extend_from_slice(&key_md5.0);
iv.extend_from_slice(&key);
iv_md5 = md5::compute(&iv);
}
}
//println!("Key: {:02x?}", key_md5);
//println!("IV: {:02x?}", iv_md5);
let end = file_size - 260;
println!("- Decrypting file...");
let decrypted_data = decrypt_aes128_cbc_pcks7(&data[16..end.try_into().unwrap()], &key_md5, &iv_md5)?;
-1
View File
@@ -68,7 +68,6 @@ pub fn extract_sdboot(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> Result<(), Bo
let entry_header: SdbootEntryHeader = entry_header_reader.read_be()?;
let offset = file.stream_position()?;
//println!("File: {} - Offset: {}, Size: {}", entry_header.name(), offset, entry_header.file_size());
file_list.push( FileEntry { name: entry_header.name(), size: entry_header.file_size(), offset });
file.seek(SeekFrom::Current(entry_header.file_size() as i64))?;
+5 -3
View File
@@ -113,10 +113,12 @@ pub fn extract_sony_bdp(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<(), B
let last_file = File::open(last_file_path.unwrap())?;
let mtk_extraction_path = app_ctx.output_dir.join(format!("{}", i));
let ctx: AppContext = AppContext {
input: InputTarget::File(last_file),
let ctx: AppContext = AppContext {
input: InputTarget::File(last_file),
output_dir: mtk_extraction_path,
options: app_ctx.options.clone()
options: app_ctx.options.clone(),
dry_run: app_ctx.dry_run,
quiet: app_ctx.quiet,
};
if let Some(result) = formats::mtk_bdp::is_mtk_bdp_file(&ctx)? {
+1 -1
View File
@@ -5,7 +5,7 @@ use binrw::BinRead;
pub struct Header {
_signature: [u8; 128],
_control_id: u32, //must be zero
pub lenght: u32, //file size
pub length: u32, //file size
pub entry_count: u32,
_pad1: [u8; 4],
build_no_bytes: [u8; 16],
+1 -1
View File
@@ -57,7 +57,7 @@ pub fn extract_tsb_bin(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<(), Bo
let hdr: Header = hdr_rdr.read_be()?;
println!("File info -\nSize: {}\nEntry count: {}\nBuild no.: {}\nEntry address: 0x{:02x}"
,hdr.lenght, hdr.entry_count, hdr.build_no(), hdr.entry_addr);
,hdr.length, hdr.entry_count, hdr.build_no(), hdr.entry_addr);
for (i, entry) in hdr.entries.iter().enumerate() {
println!("\n({}/{}) - {}, Size: {}, Offset: {}, Load address: 0x{:02x}",
+13
View File
@@ -0,0 +1,13 @@
pub const VESTEL_PARTITIONS: &[(&str, (usize, usize))] = &[
("kernel", (0x000000, 0x700000)),
("rootfs", (0x700000, 0x1A00000)),
("vendor", (0x2100000, 0x9A00000)),
("conf", (0xBB00000, 0x1E00000)),
("apd", (0xD900000, 0x1100000)),
("tee", (0xEA00000, 0x504800)),
("buf", (0xEF04800, 0x400000)),
];
pub struct VestelCtx {
pub is_encrypted: bool,
}
+109
View File
@@ -0,0 +1,109 @@
mod include;
use std::any::Any;
use crate::AppContext;
use std::fs::{self, OpenOptions};
use std::io::Write;
use crate::utils::common;
use crate::keys;
use crate::utils::aes::{decrypt_aes128_ecb};
use include::*;
pub fn is_vestel_file(app_ctx: &AppContext) -> Result<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
let file = match app_ctx.file() {Some(f) => f, None => return Ok(None)};
let file_size = file.metadata()?.len() as usize;
let header = common::read_file(&file, 0, 32)?;
if header.windows(6).any(|w| w == b"VESTEL") {
return Ok(Some(Box::new(VestelCtx { is_encrypted: false })));
}
let vestel_key = keys::VESTEL.first();
if vestel_key.is_none() {
return Ok(None);
}
let (_key_name, key_hex) = vestel_key.unwrap();
let key = hex::decode(key_hex)?;
if key.len() != 16 {
return Ok(None);
}
let key_bytes: [u8; 16] = key.try_into().map_err(|_| "Invalid key length")?;
if file_size % 16 != 0 {
return Ok(None);
}
let data = common::read_exact(&mut file.try_clone()?, file_size)?;
let decrypted = decrypt_aes128_ecb(&key_bytes, &data)?;
if decrypted.windows(6).any(|w| w == b"VESTEL") {
Ok(Some(Box::new(VestelCtx { is_encrypted: true })))
} else {
Ok(None)
}
}
pub fn extract_vestel(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<(), Box<dyn std::error::Error>> {
let mut file = app_ctx.file().ok_or("Extractor expected file")?;
let ctx = ctx.downcast::<VestelCtx>().expect("Missing context");
let file_size = file.metadata()?.len() as usize;
let data = common::read_exact(&mut file, file_size)?;
let (key_name, key_hex) = keys::VESTEL.first()
.ok_or("VESTEL key not found!")?;
let key = hex::decode(key_hex)?;
let key_len = key.len();
let key_bytes = match key.try_into() {
Ok(bytes) => bytes,
Err(_) => return Err(format!("Invalid VESTEL key length: expected 16 bytes, got {}", key_len).into()),
};
let (decrypted_data, decrypted_path) = if ctx.is_encrypted {
println!("Detected encrypted Vestel firmware, using key: {} ({})", key_name, key_hex);
println!("Decrypting firmware...");
let dec_data = decrypt_aes128_ecb(&key_bytes, &data)?;
let output_path = std::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).truncate(true).open(&output_path)?;
out_file.write_all(&dec_data)?;
(dec_data, Some(output_path))
} else {
println!("Detected Vestel firmware (unencrypted)");
(data, None)
};
let partitions = VESTEL_PARTITIONS;
println!("\nPartition count: {}", partitions.len());
for (name, (offset, size)) in partitions.iter() {
let end = offset + size;
if end > decrypted_data.len() {
println!("\nWarning: Partition {} extends beyond firmware bounds (offset=0x{:X}, size=0x{:X})", name, offset, size);
}
let segment = &decrypted_data[*offset..std::cmp::min(end, decrypted_data.len())];
println!("\n{} - Offset: 0x{:X}, Size: 0x{:X}", name, offset, size);
let output_path = std::path::Path::new(&app_ctx.output_dir).join(format!("{}.bin", name));
fs::create_dir_all(&app_ctx.output_dir)?;
let mut out_file = OpenOptions::new().write(true).create(true).truncate(true).open(output_path)?;
out_file.write_all(segment)?;
println!("- Saved {}.bin", name);
}
if let Some(path) = decrypted_path {
if !app_ctx.has_option("vestel:keep_decrypted") {
fs::remove_file(&path)?;
}
}
Ok(())
}
+6 -1
View File
@@ -167,4 +167,9 @@ pub static EPK: &[(&str, &str)] = &[
("52A208FA24E7E70730A40999B1C22C148F4920484BC50B515D243E35D14689F1", "o24n - LX webOS 10 (2025)"),
("1FB2C3B789D5EA48ED16E79A0343986C691DACEC872BB07787D0F722AF5D1E2C", "lm21ann - MStar webOS 8 (2023)"),
];
];
//Vestel keys
pub static VESTEL: &[(&str, &str)] = &[
("VESTEL-MB180/MB130/MB181", "12345678123456781234567812345678"),
];
+104 -22
View File
@@ -1,21 +1,51 @@
mod error;
mod formats;
mod keys;
mod utils;
use clap::Parser;
use std::path::{PathBuf};
use std::path::PathBuf;
use std::io::{self, Seek, SeekFrom};
use std::fs::{self, File};
use crate::formats::{Format, get_registry};
use crate::error::UnixtractError;
#[derive(Parser, Debug)]
#[command(
name = "unixtract",
version,
about = "Firmware extractor for various file formats",
long_about = "unixtract analyzes and extracts firmware package formats commonly found \
in TVs, Blu-Ray players, and AV devices. It supports 35+ different formats \
with built-in decryption and decompression capabilities."
)]
struct Args {
input_target: String,
/// The target file or directory to analyze/extract
#[arg(required_unless_present = "list-formats")]
input_target: Option<String>,
/// Folder to save extracted files to (default: _<INPUT_TARGET>)
output_directory: Option<String>,
///format specific options
/// Format-specific or global options (can be used multiple times)
#[arg(short, long)]
options: Vec<String>,
/// List supported formats and exit
#[arg(long)]
list_formats: bool,
/// Only detect the format, do not extract
#[arg(long)]
dry_run: bool,
/// Quiet mode — suppress non-error output
#[arg(short, long)]
quiet: bool,
/// Verbose mode — show detailed progress information
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
}
pub enum InputTarget {
@@ -27,7 +57,10 @@ pub struct AppContext {
pub input: InputTarget,
pub output_dir: PathBuf,
pub options: Vec<String>,
pub dry_run: bool,
pub quiet: bool,
}
impl AppContext {
pub fn file(&self) -> Option<&File> {
match &self.input {
@@ -49,32 +82,63 @@ impl AppContext {
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("unixtract Firmware extractor");
let args = Args::parse();
let target_path_str = args.input_target;
println!("Input target: {}", target_path_str);
let target_path = PathBuf::from(&target_path_str);
let output_path_str = if args.output_directory.is_some() {
args.output_directory.unwrap()
// Initialize logger based on verbosity level
let log_level = if args.quiet {
"error"
} else {
format!("_{}", target_path.file_name().and_then(|s| s.to_str()).unwrap())
match args.verbose {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
}
};
println!("Output directory: {}", output_path_str);
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level))
.format_timestamp(None)
.init();
// Handle --list-formats
if args.list_formats {
let formats = get_registry();
println!("Supported formats ({} total):", formats.len());
for (i, fmt) in formats.iter().enumerate() {
println!(" {:2}. {}", i + 1, fmt.name);
}
return Ok(());
}
log::info!("unixtract Firmware extractor v{}", env!("CARGO_PKG_VERSION"));
let target_path_str = args.input_target.as_deref().unwrap_or("");
log::info!("Input target: {}", target_path_str);
let target_path = PathBuf::from(target_path_str);
let output_path_str = if let Some(ref out) = args.output_directory {
out.clone()
} else {
format!("_{}", target_path.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| UnixtractError::Other("Invalid input file name".to_string()))?)
};
log::info!("Output directory: {}", output_path_str);
let output_directory_path = PathBuf::from(&output_path_str);
if output_directory_path.exists() {
if output_directory_path.is_dir() {
let is_empty = fs::read_dir(&output_directory_path)?.next().is_none();
if !is_empty {
println!("\nWarning: Output folder already exists and is NOT empty! Files may be overwritten!");
println!("Press Enter if you want to continue...");
if !is_empty && !args.quiet {
log::warn!("Output folder already exists and is NOT empty! Files may be overwritten.");
eprintln!("Warning: Output folder already exists and is NOT empty! Files may be overwritten!");
eprintln!("Press Enter if you want to continue...");
io::stdin().read_line(&mut String::new())?;
}
}
}
// === DEFAULT MODE (original behavior) ===
let app_ctx;
if target_path.is_file() {
@@ -83,37 +147,55 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
input: InputTarget::File(file),
output_dir: output_directory_path,
options: args.options,
dry_run: args.dry_run,
quiet: args.quiet,
};
} else if target_path.is_dir() {
app_ctx = AppContext {
input: InputTarget::Directory(target_path),
output_dir: output_directory_path,
options: args.options,
dry_run: args.dry_run,
quiet: args.quiet,
};
} else {
return Err("Invalid input path!".into());
}
let formats: Vec<Format> = get_registry();
println!("Loaded {} formats!", formats.len());
log::info!("Loaded {} formats", formats.len());
for format in formats {
if let Some(ctx) = (format.detector_func)(&app_ctx)? {
println!("\n{} detected!", format.name);
log::info!("{} detected!", format.name);
if !app_ctx.quiet {
println!("\n{} detected!", format.name);
}
//reset seek of the file if present
if app_ctx.dry_run {
if !app_ctx.quiet {
println!("Dry run — skipping extraction.");
}
return Ok(());
}
// Reset seek of the file if present
if let Some(mut file) = app_ctx.file() {
file.seek(SeekFrom::Start(0))?;
}
(format.extractor_func)(&app_ctx, ctx)?;
//extractor returned with no error
println!("\nExtraction finished! Saved extracted files to {}", output_path_str);
// Extractor returned with no error
if !app_ctx.quiet {
println!("\nExtraction finished! Saved extracted files to {}", output_path_str);
}
return Ok(());
}
}
println!("\nInput format not recognized!");
Ok(())
if !app_ctx.quiet {
println!("\nInput format not recognized!");
}
Err("Unrecognized input format".into())
}
+2 -1
View File
@@ -3,4 +3,5 @@ pub mod aes;
pub mod lzop;
pub mod sparse;
pub mod compression;
pub mod global;
pub mod global;
pub mod path_sanitize;
+69 -9
View File
@@ -1,31 +1,44 @@
use aes::Aes128;
//! AES encryption/decryption utilities.
//!
//! Provides centralized implementations for all AES modes used across the
//! various firmware format extractors. Instead of duplicating AES logic in
//! each format module, all AES operations should go through this module.
use cbc::{Decryptor, cipher::{block_padding::Pkcs7, block_padding::NoPadding, BlockDecryptMut, KeyIvInit}};
type Aes128CbcDec = Decryptor<Aes128>;
use aes::{Aes128, Aes256};
use cbc::{Decryptor as CbcDecryptor, cipher::{block_padding::Pkcs7, block_padding::NoPadding, BlockDecryptMut, KeyIvInit}};
use ecb::{Decryptor as EcbDecryptor, cipher::{KeyInit, generic_array::GenericArray}};
type Aes128CbcDec = CbcDecryptor<Aes128>;
type Aes128EcbDec = EcbDecryptor<Aes128>;
type Aes256EcbDec = EcbDecryptor<Aes256>;
// ── AES-128 CBC ─────────────────────────────────────────────────────────────
/// Decrypt data using AES-128-CBC with PKCS7 padding.
pub fn decrypt_aes128_cbc_pcks7(encrypted_data: &[u8], key: &[u8; 16], iv: &[u8; 16]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut data = encrypted_data.to_vec();
let decryptor = Aes128CbcDec::new(key.into(), iv.into());
let decrypted = decryptor.decrypt_padded_mut::<Pkcs7>(&mut data)
.map_err(|e| format!("!!Decryption error!!: {:?}", e))?;
.map_err(|e| format!("AES-128-CBC-PKCS7 decryption error: {:?}", e))?;
Ok(decrypted.to_vec())
}
/// Decrypt data using AES-128-CBC with no padding.
pub fn decrypt_aes128_cbc_nopad(encrypted_data: &[u8], key: &[u8; 16], iv: &[u8; 16]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut data = encrypted_data.to_vec();
let decryptor = Aes128CbcDec::new(key.into(), iv.into());
let decrypted = decryptor
.decrypt_padded_mut::<NoPadding>(&mut data)
.map_err(|e| format!("UnpadError: {:?}", e))?;
.map_err(|e| format!("AES-128-CBC-NoPad decryption error: {:?}", e))?;
Ok(decrypted.to_vec())
}
use ecb::{Decryptor as EcbDecryptor, cipher::{KeyInit, generic_array::GenericArray}};
type Aes128EcbDec = EcbDecryptor<Aes128>;
// ── AES-128 ECB ─────────────────────────────────────────────────────────────
/// Decrypt data using AES-128-ECB (raw block decryption, no padding).
pub fn decrypt_aes128_ecb(key: &[u8; 16], ciphertext: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut buffer = ciphertext.to_vec();
@@ -36,4 +49,51 @@ pub fn decrypt_aes128_ecb(key: &[u8; 16], ciphertext: &[u8]) -> Result<Vec<u8>,
}
Ok(buffer)
}
}
// ── AES-256 ECB ─────────────────────────────────────────────────────────────
/// Decrypt data using AES-256-ECB (raw block decryption, no padding).
pub fn decrypt_aes256_ecb(key: &[u8; 32], ciphertext: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut buffer = ciphertext.to_vec();
let mut decryptor = Aes256EcbDec::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)
}
// ── AES ECB auto-detect key size ────────────────────────────────────────────
/// Decrypt data using AES-ECB, automatically selecting AES-128 or AES-256
/// based on the key length (16 bytes → AES-128, 32 bytes → AES-256).
///
/// Returns an error if the key length is neither 16 nor 32 bytes.
pub fn decrypt_aes_ecb_auto(key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut buffer = ciphertext.to_vec();
match key.len() {
16 => {
let key_array: [u8; 16] = key.try_into()?;
let mut decryptor = Aes128EcbDec::new(&key_array.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));
}
}
32 => {
let key_array: [u8; 32] = key.try_into()?;
let mut decryptor = Aes256EcbDec::new(&key_array.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));
}
}
_ => return Err(format!("Invalid AES key length: {} (expected 16 or 32)", key.len()).into()),
}
Ok(buffer)
}
-1
View File
@@ -50,7 +50,6 @@ pub fn unlzop_to_file(data: &[u8], file_path: PathBuf) -> Result<(), Box<dyn std
if segment_header.compressed_size > segment_header.uncompressed_size {
println!("{:?}", segment_header);
}
//println!("{:?}", segment_header);
if segment_header.uncompressed_size == 0 {
break
}
+77
View File
@@ -0,0 +1,77 @@
//! Path sanitization utilities.
//!
//! Firmware images often embed file paths (e.g., partition names, file names)
//! that are used directly as output paths. A maliciously crafted firmware image
//! could contain paths like `../../etc/passwd` to write files outside the
//! intended output directory. This module provides sanitization to prevent
//! path traversal attacks.
use std::path::{Component, Path, PathBuf};
use crate::error::{UnixtractError, Result};
/// Sanitize a path extracted from a firmware image to prevent path traversal.
///
/// This function:
/// 1. Removes any leading `/` (absolute path)
/// 2. Removes `..` components that would escape the base directory
/// 3. Removes empty components
/// 4. Collapses redundant `.` components
///
/// Returns the sanitized relative path, or an error if the path would escape
/// the base directory after sanitization.
#[allow(dead_code)]
pub fn sanitize_path(path: &str) -> Result<PathBuf> {
let raw = Path::new(path);
let mut sanitized = PathBuf::new();
for component in raw.components() {
match component {
Component::CurDir => {
// Skip "." components
}
Component::Normal(c) => {
sanitized.push(c);
}
Component::ParentDir => {
// Try to pop the last component, but if we can't, just skip it
// rather than allowing traversal
if !sanitized.pop() {
// Can't go up — skip the ".." silently
}
}
Component::RootDir | Component::Prefix(_) => {
// Strip absolute path prefixes
}
}
}
// Final safety check: verify no ".." remains
if sanitized.components().any(|c| matches!(c, Component::ParentDir)) {
return Err(UnixtractError::UnsafePath {
path: PathBuf::from(path),
});
}
Ok(sanitized)
}
/// Construct a safe output path by joining a base directory with a sanitized
/// filename or relative path extracted from firmware.
///
/// Returns an error if the resulting path would be outside `base_dir`.
#[allow(dead_code)]
pub fn safe_output_path(base_dir: &Path, firmware_path: &str) -> Result<PathBuf> {
let sanitized = sanitize_path(firmware_path)?;
let result = base_dir.join(&sanitized);
// Verify the result doesn't escape the base directory
let result_str = result.to_string_lossy();
let base_str = base_dir.to_string_lossy();
if !result_str.starts_with(base_str.as_ref()) {
return Err(UnixtractError::UnsafePath {
path: PathBuf::from(firmware_path),
});
}
Ok(result)
}
+1 -1
View File
@@ -44,7 +44,7 @@ pub fn unsparse_to_file(data: &[u8], file_path: PathBuf) -> Result<(), Box<dyn s
} else if chunk_header.chunk_type == 0xCAC2 { // "fill" type chunk (fill size with a value)
if chunk_data.len() != 4 {
return Err("Inavlid lenght of FILL chunk!".into());
return Err("Invalid length of FILL chunk!".into());
}
let fill_size = (chunk_header.chunk_size * file_header.block_size) / 4;
let fill_data = chunk_data.repeat(fill_size as usize);