This commit is contained in:
Pari Malam
2026-07-15 09:50:27 +08:00
parent eff7942ba1
commit e891bf5ecd
18 changed files with 5408 additions and 294 deletions
+12
View File
@@ -0,0 +1,12 @@
# Cross-compilation settings.
#
# When building the Windows GNU target from Linux, point Cargo at the
# mingw-w64 toolchain (install via `apt install gcc-mingw-w64-x86-64`).
# These settings are ignored on hosts that build natively.
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
ar = "x86_64-w64-mingw32-ar"
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
+61
View File
@@ -0,0 +1,61 @@
name: rust
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: Build ${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
artifact: unixtract-linux
bin: unixtract
- target: x86_64-pc-windows-gnu
artifact: unixtract-windows.exe
bin: unixtract.exe
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install mingw-w64 (Windows target)
if: matrix.target == 'x86_64-pc-windows-gnu'
run: sudo apt-get update && sudo apt-get install -y gcc-mingw-w64-x86-64
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Rename binary
run: cp "target/${{ matrix.target }}/release/${{ matrix.bin }}" "${{ matrix.artifact }}"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: ${{ matrix.artifact }}
if-no-files-found: error
+4 -1
View File
@@ -6,4 +6,7 @@ wvenv/
dist/ dist/
firmware/ firmware/
device/ device/
dist/ dist/
3rd_party/
.vscode/
*.exe
+140 -189
View File
@@ -1,232 +1,183 @@
# unixtract # unixtract
Extractor for various file formats.
This project is a fork of [theubusu/unixtract](https://github.com/theubusu/unixtract), adding extra features and improvements.
This is a tool that is able to extract the contents of various firmware package formats, mostly from TVs and AV devices.
Built in Rust, and made to not depend on any external dependancies, only Rust crates. This way it can work on Windows, Linux and MacOS and even Android.
Please note that this project is still under active development and errors may occur. Feel free to make an issue in that case, or if you have any feature request.
**PLEASE NOTE** - this program is NOT, and will never be designed for re-packing the extracted files!
## About this fork > A fast, dependency-free firmware extractor for TVs, Blu-ray players, set-top boxes, and other AV devices.
This fork extends the upstream project with additional features and bug fixes. For the original README and format support details, see the [upstream repository](https://github.com/theubusu/unixtract).
# Installation `unixtract` analyzes and unpacks a wide range of proprietary firmware package formats, with built-in decryption and decompression. It is written entirely in Rust with no external runtime dependencies, so a single binary runs on **Windows, Linux, macOS, and Android**.
You can download the latest auto build for Windows and Linux x86-64 from [here](https://nightly.link/theubusu/unixtract/workflows/rust/main).
Or, build from source, by downloading the code or cloning the respository and running `cargo build --release`. The binary will be saved in `target/release`.
# Usage This project is a fork of [theubusu/unixtract](https://github.com/theubusu/unixtract), adding extra formats, features, and fixes.
`unixtract [OPTIONS] <INPUT_TARGET> [OUTPUT_FOLDER]`
Arguments:
`<INPUT_TARGET>` - The target to analyze/extract.
`[OUTPUT_FOLDER]` - Folder to save extracted files to.
If an output folder is not provided, extracted files will be saved in folder `_<INPUT_TARGET>`.
Options:
`-o, --options <OPTIONS>` - Format specific or global(for all formats that implement it) options, see the list below for format specific options. You can use this multiple times to activate multiple options.
## Global options > [!NOTE]
`dump_dec_hdrs` - For formats with an encrypted header - dump the decrypted header(s). > This project is under active development — errors may occur. Bug reports and feature requests are welcome via the issue tracker.
# Supported formats > [!IMPORTANT]
## Amlogic burning image > `unixtract` is an **extraction** tool only. It is **not**, and will never be, designed to re-pack extracted files.
**Used in:** Android TVs and Boxes
**Notes:** V1 format is not supported because of the lack of sample file.
**Thanks to:** https://github.com/7Ji/ampack
## Android OTA payload.bin ---
**Used in:** Android devices, smartphones, TVs
**Notes:** Some compression methods may not be supported.
**Thanks to:** https://android.googlesource.com/platform/system/update_engine/+/HEAD/update_metadata.proto
## BDL ## Table of Contents
**Used in:** Enterprise HP Printers
**Notes:** None, all files should be supported
## CD5 - [Features](#features)
**Used in:** Some Samsung TV tuners, and possibly other Irdeto(?)-based tuners - [Installation](#installation)
**Notes:** Decryption is not supported. - [Usage](#usage)
- [Supported Formats](#supported-formats)
- [Format Options](#format-options)
- [Notes on Keys](#notes-on-keys)
- [License](#license)
## EPK v1 ---
**Used in:** LG TVs before ~2010
**Notes:** None, all files should be supported
**Thanks to:** https://github.com/openlgtv/epk2extract
## EPK v2 ## Features
**Used in:** LG TVs since ~2010
**Notes:** **Depends on keys** - see keys.rs (most common keys should be included)
**Thanks to:** https://github.com/openlgtv/epk2extract
**Options:**
※ Support `dump_dec_hdrs` option
## EPK v3 - **35+ firmware formats** across major TV/AV silicon vendors (MStar, MediaTek, Novatek, Amlogic, Broadcom…).
**Used in:** LG webOS-based TVs - **Built-in decryption** (AES, DES, ECB/CBC, RSA) and **decompression** (LZO, LZ4, LZMA/XZ, zlib/gzip, bzip2, zstd, LZHS, sparse).
**Notes:** **Depends on keys** - see keys.rs - **Recursive extraction** — container formats automatically unpack their inner payloads.
**Thanks to:** https://github.com/openlgtv/epk2extract - **NAND-aware** — handles raw NAND dumps, OOB/spare stripping, and UBI/UBIFS rootfs images.
**Options:** - **Single static binary** — no interpreters, no system libraries.
※ Support `dump_dec_hdrs` option - **Bulk mode** — process an entire directory of firmware in one run.
## Funai BDP ## Installation
**Used in:** Funai & Funai-made Philips Blu-Ray player/HTS (USA market)
**Notes:** N/A
## Funai MStar ### Prebuilt binaries
**Used in:** MStar-based Funai & Funai-made Philips TVs (USA market)
**Notes:** Inner SoC part is extracted with mstar_secure_old
## Funai UPG Download the latest automated build for Windows and Linux x86-64 from the [nightly builds](https://nightly.link/Ap0dexMe0/unixtract/workflows/rust/main).
**Used in:** Some Funai TVs
**Notes:** **Depends on keys** - see keys.rs.
## Funai UPG PHL ### From source
**Used in:** Funai & Funai-made Philips TVs (USA market)
**Notes:** **Depends on keys** - see keys.rs (most common keys should be included).
## GX DVB ```sh
**Used in:** Cheap NationalChip GX-based DVB tuners git clone https://github.com/Ap0dexMe0/unixtract
**Notes:** None, all files should be supported cd unixtract
cargo build --release
```
## INVINCIBLE_IMAGE The resulting binary is written to `target/release/unixtract`.
**Used in:** LG Broadcom-based Blu-Ray players
**Notes:** Key ID 1 (<2010) is not supported.
Tip: if you have split ROM (.ROM-00 and .ROM-01), extract both into the same folder so they get combined.
## MSD 1.0 ## Usage
**Used in:** Samsung TVs 2013-2015
**Notes:** **Depends on keys** - see keys.rs
**Thanks to:** https://github.com/bugficks/msddecrypt
**Options:**
`msd10:save_cmac` - Save CMAC data for files that is skipped by default.
`msd:print_ouith` - Prints the entire parsed OUITH header.
※ Support `dump_dec_hdrs` option
## MSD 1.1 ```sh
**Used in:** Samsung TVs 2016+ unixtract [OPTIONS]
**Notes:** **Depends on keys** - see keys.rs (keys 2015-2018, 2020 included) ```
**Thanks to:** https://github.com/bugficks/msddecrypt
**Options:**
`msd:print_ouith` - Prints the entire parsed OUITH header.
※ Support `dump_dec_hdrs` option
## MStar upgrade bin ### Input modes (mutually exclusive)
**Used in:** Many MStar-based TVs (Hisense, Toshiba...)
**Notes:** All files should be supported, includes lzop, lz4, lzma, sparse_write support
**Options:**
`mstar:keep_unknown` - Save data with unknown destination.
※ Support `dump_dec_hdrs` option (will save the script)
## MStar upgrade bin (Secure, old) | Flag | Description |
**Used in:** Older MStar-based TVs with Secure upgrade mode (encrypted+signed) | --- | --- |
**Notes:** Only default upgrade key is supported. This use the extractor above after decrypting. | `--file-input <FILE>` | Extract a single firmware binary |
**Options:** | `--dir-input <DIR>` | Bulk-process every firmware binary in a directory |
`mstar_secure_old:keep_decrypted` - Keep decrypted file (it will be deleted by default).
## MediaTek BDP ### Options
**Used in:** Many MediaTek-based Blu-Ray players (LG, Samsung, Philips, Panasonic...)
**Notes:** Some older files may fail to extract
## MediaTek PKG (New) | Flag | Description |
**Used in:** Newer MediaTek-based TVs (TCL, Hisense, Sony, Philips, CVT...) | --- | --- |
**Notes:** **Depends on keys** - see keys.rs (Keys for Philips and Sony included) | `--output <PATH>` | Output path for extracted data (default: `_<INPUT>`) |
**Thanks to:** https://github.com/theubusu/mtkdec | `--lazy-run` | Detect/scan only — skip extraction (fast analysis) |
**Options:** | `--build-prop` | Extract and display firmware build properties and metadata |
`mtk_pkg:no_del_comp` - Don't delete LZHS compressed partition file after decompressing. | `--dump-keys` | Dump the built-in decryption keys |
※ Support `dump_dec_hdrs` option | `--list-formats` | List all supported formats and exit |
| `-v, --verbose` | Increase verbosity (repeat for more detail) |
| `-h, --help` | Print help information |
| `-V, --version` | Print version information |
## MediaTek PKG (Old) ### Examples
**Used in:** Older MediaTek-based TVs (Philips, Sony, Hisense...)
**Notes:** All files should be supported, decryption + decompression
**Options:**
`mtk_pkg:no_del_comp` - Don't delete LZHS compressed partition file after decompressing.
※ Support `dump_dec_hdrs` option
## Novatek PKG (NFWB) ```sh
**Used in:** Some older Novatek-based TVs (LG, Philips) # Single file
**Notes:** None, all files should be supported. unixtract --file-input firmware.bin
unixtract --file-input firmware.bin --output extracted/ --verbose
## Novatek TIMG # Bulk directory
**Used in:** Newer Novatek-based TVs (Philips(TPVision), Hisense, TCL...) unixtract --dir-input firmware_dump/ --output out/
**Notes:** None, all files should be supported.
## Onkyo # Quick analysis without extracting
**Used in:** Onkyo AVRs and other AV devices unixtract --file-input firmware.bin --lazy-run
**Notes:** Newer files seem to use a different encryption and are not (yet) supported. ```
**Thanks to:** http://divideoverflow.com/2014/04/decrypting-onkyo-firmware-files/
**Options:**
※ Support `dump_dec_hdrs` option
## Panasonic Blu-Ray (PANA_DVD.FRM, PANA_ESD.FRM, PANAEUSB.FRM) ## Supported Formats
**Used in:** Panasonic Blu-Ray Players and Recorders
**Notes:** **Depends on keys** - see keys.rs (Included keys should work for 99% of players released in and before 2014, and some released in 2018), Note that there is currently an issue with MAIN in some very ancient files not extracting correctly.
**Options:**
`pana_dvd:split_main` - Automatically split the MAIN module into seperate partitions.
※ Support `dump_dec_hdrs` option
## Philips UPG (Autorun.upg, 2SWU3TXV) > Entries marked **keys** depend on decryption keys — see [`keys.rs`](src/keys.rs). Most common keys are bundled.
**Used in:** Philips pre-TPVision TVs 200?-2013 and some Sony TVs > Entries marked **hdrs** support the `dump_dec_hdrs` option.
**Notes:** **Depends on keys** - see keys.rs
**Thanks to:** https://github.com/frederic/pflupg-tool
**Options:**
`pfl_upg:no_extract_inner_upg` - Do not automatically extract inner UPGs. (Warning: this can cause file collisions sometimes!)
## Philips BDP ### TV / SoC firmware
**Used in:** Philips MediaTek-based Blu-ray players/Home theatre systems
**Notes:** The main partition (ID 0) can be sometimes encrypted, and there is no good way to detect that. So if MTK BDP extraction fails, try running with `philips_bdp:decrypt` option.
**Options:**
`philips_bdp:decrypt` - Decrypt main partition
## PUP | Format | Used in | Notes |
**Used in:** Sony PlayStation 4/5 | --- | --- | --- |
**Notes:** File has to be decrypted. | **MStar upgrade bin** | Many MStar-based TVs (Hisense, Toshiba…) | LZOP, LZ4, LZMA, sparse-write support · hdrs |
**Thanks to:** https://github.com/Zer0xFF/ps4-pup-unpacker | **MStar upgrade bin (Secure, old)** | Older MStar TVs with secure (encrypted+signed) upgrades | Default upgrade key only |
| **MStar UNFD NAND dump** | Raw MStar NAND dumps prefixed with `MSTARSEMIUNFDCIS` (e.g. `TH58NVG2S3HTA00.bin`) | Derives NAND geometry; carves boot banks, `rootfs_ubi.bin`, and `nand_data.bin`; recurses into inner formats; writes `cis_info.txt` |
| **UBI / UBIFS NAND rootfs** | NAND Linux rootfs images (`UBI#` header), e.g. carved `rootfs_ubi.bin` | Strips interleaved NAND OOB, tolerates vendor header quirks, reconstructs volumes, CRC-validated linear UBIFS scan (LZO/zlib/zstd) |
| **MediaTek PKG (New)** | Newer MediaTek TVs (TCL, Hisense, Sony, Philips, CVT…) | keys (Philips, Sony) · hdrs |
| **MediaTek PKG (Old)** | Older MediaTek TVs (Philips, Sony, Hisense…) | Full decrypt + decompress · hdrs |
| **Novatek PKG (NFWB)** | Older Novatek TVs (LG, Philips) | All files supported |
| **Novatek TIMG** | Newer Novatek TVs (TPVision, Hisense, TCL…) | All files supported |
| **Amlogic burning image** | Android TVs and boxes | V1 not supported (no sample) · thanks to [ampack](https://github.com/7Ji/ampack) |
| **EPK v1** | LG TVs before ~2010 | thanks to [epk2extract](https://github.com/openlgtv/epk2extract) |
| **EPK v2** | LG TVs since ~2010 | keys · hdrs · thanks to [epk2extract](https://github.com/openlgtv/epk2extract) |
| **EPK v3** | LG webOS TVs | keys · hdrs · thanks to [epk2extract](https://github.com/openlgtv/epk2extract) |
| **MSD 1.0** | Samsung TVs 20132015 | keys · hdrs · thanks to [msddecrypt](https://github.com/bugficks/msddecrypt) |
| **MSD 1.1** | Samsung TVs 2016+ | keys (20152018, 2020) · hdrs · thanks to [msddecrypt](https://github.com/bugficks/msddecrypt) |
| **Samsung (`*.img.sec` folder)** | Samsung TVs pre-2013 | keys · thanks to [samygo-patcher](https://github.com/george-hopkins/samygo-patcher) |
| **Philips UPG** (`Autorun.upg`, `2SWU3TXV`) | Philips pre-TPVision TVs 200?2013, some Sony TVs | keys · thanks to [pflupg-tool](https://github.com/frederic/pflupg-tool) |
| **Roku** | Roku TVs / players | Some inner images remain encrypted |
| **GX DVB** | Cheap NationalChip GX-based DVB tuners | All files supported |
| **CD5** | Some Samsung TV tuners / Irdeto-based tuners | Decryption not supported |
| **TSB Bin** | Older Toshiba TVs | hdrs |
## Roku ### Blu-ray / AV device firmware
**Used in:** Roku TV's/players
**Notes:** The contents of the update file can be extracted, but some firmware images contained inside are additionally encrypted, and they cannot be decrypted as of now.
## RUF | Format | Used in | Notes |
**Used in:** Samsung Broadcom-based Blu-Ray players | --- | --- | --- |
**Notes:** **Depends on keys** - see keys.rs | **MediaTek BDP** | MediaTek Blu-ray players (LG, Samsung, Philips, Panasonic…) | Some older files may fail |
| **Philips BDP** | Philips MediaTek-based Blu-ray players / HTS | Main partition may be encrypted — try `philips_bdp:decrypt` |
| **Sony BDP** | Sony MediaTek-based Blu-ray players | keys (up to MSB29) · thanks to [s390-firmware](http://malcolmstagg.com/bdp/s390-firmware.html) |
| **Funai BDP** | Funai / Funai-made Philips Blu-ray & HTS (USA) | — |
| **Funai MStar** | MStar-based Funai / Philips TVs (USA) | Inner SoC part via `mstar_secure_old` |
| **Funai UPG** | Some Funai TVs | keys |
| **Funai UPG PHL** | Funai / Philips TVs (USA) | keys |
| **Panasonic Blu-ray** (`PANA_DVD/ESD/EUSB.FRM`) | Panasonic Blu-ray players & recorders | keys (≤2014, some 2018) · hdrs |
| **INVINCIBLE_IMAGE** | LG Broadcom Blu-ray players | Key ID 1 (<2010) unsupported; extract split `.ROM-00/.ROM-01` together |
| **RUF** | Samsung Broadcom Blu-ray players | keys |
| **RVP / MVP** | Sharp Blu-ray players / recorders | Older XOR-encrypted types only |
| **Onkyo** | Onkyo AVRs and AV devices | Newer encryption unsupported · hdrs · thanks to [divideoverflow](http://divideoverflow.com/2014/04/decrypting-onkyo-firmware-files/) |
## RVP/MVP ### Panasonic TV firmware
**Used in:** Sharp Blu-Ray players/recorders
**Notes:** Only the older types of files are supported (XOR-encrypted)
## Samsung (Folder with ***.img.sec) | Format | Used in | Notes |
**Used in:** Samsung TVs pre 2013 | --- | --- | --- |
**Notes:** **Depends on keys** - see keys.rs | **SDDL.SEC** | Panasonic TVs | Based on [sddl_dec](https://github.com/theubusu/sddl_dec) |
**Thanks to:** https://github.com/george-hopkins/samygo-patcher | **SDBoot** | Panasonic TVs (SD boot) | Single known sample — support may vary |
| **SDImage** (`SDImage.bin`) | Some 2010 USA Panasonic TVs | Decryption not yet supported |
## SDBoot ### Generic / other
**Used in:** Panasonic TVs SD boot
**Notes:** There is only one known sample, so support may vary.
**Base:** https://github.com/theubusu/sddl_dec
## SDDL.SEC | Format | Used in | Notes |
**Used in:** Panasonic TVs | --- | --- | --- |
**Notes:** None, all files should be supported. | **Android OTA `payload.bin`** | Android devices, phones, TVs | Some compression methods unsupported |
**Options:** | **Raw eMMC user area** | eMMC dumps with `0x5840` partition descriptors | Dumps `<partition>.bin` + `partition_map.txt` |
`sddl_sec:save_extra` - Save SDIT.FDI and .TXT files that are not extracted by default. | **BDL** | Enterprise HP printers | All files supported |
`sddl_sec:split_peaks` - Split PEAKS module into partitions (only on older files). This will also automatically decompress compressed partitions. | **PUP** | Sony PlayStation 4/5 | Requires a decrypted file · thanks to [ps4-pup-unpacker](https://github.com/Zer0xFF/ps4-pup-unpacker) |
`sddl_sec:no_decomp_peaks` - Do not automatically decompress partitions when splitting PEAKS with above option. | **SLP** | Samsung Tizen-based NX cameras | All files supported |
**Base:** https://github.com/theubusu/sddl_dec
## SDImage (SDImage.bin) ## Format Options
**Used in:** Some 2010 USA Panasonic TVs
**Notes:** Decryption is not yet supported.
## SLP Format-specific options are passed by name. Example: `mstar:keep_unknown`.
**Used in:** Samsung Tizen-based NX series cameras
**Notes:** None, all files should be supported.
## Sony BDP | Option | Effect |
**Used in:** Sony MediaTek-based Blu-Ray players | --- | --- |
**Notes:** **Depends on keys** - see keys.rs (Platforms up to MSB29 are supported) | `mstar:keep_unknown` | Save data with unknown destination |
**Thanks to:** http://malcolmstagg.com/bdp/s390-firmware.html | `mstar_secure_old:keep_decrypted` | Keep the decrypted file (deleted by default) |
| `msd10:save_cmac` | Save CMAC data skipped by default |
| `msd:print_ouith` | Print the entire parsed OUITH header |
| `mtk_pkg:no_del_comp` | Keep LZHS compressed partition after decompressing |
| `pana_dvd:split_main` | Split the MAIN module into separate partitions |
| `pfl_upg:no_extract_inner_upg` | Do not auto-extract inner UPGs (may avoid collisions) |
| `philips_bdp:decrypt` | Decrypt the main partition |
| `sddl_sec:save_extra` | Save `SDIT.FDI` and `.TXT` files skipped by default |
| `sddl_sec:split_peaks` | Split the PEAKS module into partitions (older files) |
| `sddl_sec:no_decomp_peaks` | Do not auto-decompress when splitting PEAKS |
## TSB Bin ## Notes on Keys
**Used in:** Older Toshiba TVs
**Notes:** None, all files should be supported.
**Options:**
※ Support `dump_dec_hdrs` option
# License Many formats require decryption keys stored in [`src/keys.rs`](src/keys.rs). The most common publicly known keys are bundled. Use `--dump-keys` to list what is available. If extraction of a key-dependent format fails, the required key may simply not be present.
Licensed under GNU GPL v3.
## License
Licensed under the **GNU General Public License v3.0**.
+63 -27
View File
@@ -1,35 +1,71 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#
# Cross-compile unixtract for multiple targets and collect the binaries into
# ./dist. Run from the repository root:
#
# ./build.sh # build the default target set
# ./build.sh <target> ... # build only the given rustc target triples
#
# Requirements (Debian/Ubuntu example):
# rustup # toolchain manager
# sudo apt install gcc-mingw-w64-x86-64 # for x86_64-pc-windows-gnu
# sudo apt install gcc-aarch64-linux-gnu # for aarch64-unknown-linux-gnu (optional)
#
set -euo pipefail set -euo pipefail
echo "Building unixtract (Linux + Windows)..." # --- Target set ------------------------------------------------------------
# Override by passing target triples as arguments.
DEFAULT_TARGETS=(
x86_64-unknown-linux-gnu
x86_64-pc-windows-gnu
)
# Clean first if [ "$#" -gt 0 ]; then
cargo clean TARGETS=("$@")
else
# Build Linux TARGETS=("${DEFAULT_TARGETS[@]}")
cargo build --target x86_64-unknown-linux-gnu --release
# Build Windows (GNU)
cargo build --target x86_64-pc-windows-gnu --release
# Prepare dist folder
mkdir -p dist
echo "Collecting binaries..."
# Linux binary
LINUX_BIN="target/x86_64-unknown-linux-gnu/release/unixtract"
if [ -f "$LINUX_BIN" ]; then
cp "$LINUX_BIN" "dist/unixtract-linux"
fi fi
# Windows binary BIN_NAME="unixtract"
WINDOWS_BIN="target/x86_64-pc-windows-gnu/release/unixtract.exe" DIST_DIR="dist"
if [ -f "$WINDOWS_BIN" ]; then
cp "$WINDOWS_BIN" "dist/unixtract-windows.exe"
fi
echo "Cleaning target directory..." echo "==> Building ${BIN_NAME} for: ${TARGETS[*]}"
rm -rf target
echo "Done. Binaries are in ./dist" mkdir -p "$DIST_DIR"
for target in "${TARGETS[@]}"; do
echo
echo "==> Target: $target"
# Ensure the target's std library is installed.
if ! rustup target list --installed | grep -qx "$target"; then
echo " Installing rust std for $target ..."
rustup target add "$target"
fi
cargo build --release --target "$target"
# Work out the produced binary name (Windows targets get a .exe suffix).
case "$target" in
*windows*) src="target/$target/release/${BIN_NAME}.exe" ;;
*) src="target/$target/release/${BIN_NAME}" ;;
esac
if [ ! -f "$src" ]; then
echo " !! Expected binary not found: $src" >&2
exit 1
fi
# Friendly output name, e.g. unixtract-x86_64-pc-windows-gnu.exe
case "$target" in
*windows*) dst="$DIST_DIR/${BIN_NAME}-${target}.exe" ;;
*) dst="$DIST_DIR/${BIN_NAME}-${target}" ;;
esac
cp "$src" "$dst"
echo " -> $dst"
done
echo
echo "==> Done. Binaries are in ./${DIST_DIR}"
ls -la "$DIST_DIR"
+96
View File
@@ -0,0 +1,96 @@
435552455f53544f52455f46494c455f
fafe67a1b53bbb8b931fecbaac0f0c0e
0007FF4154534D92FC55AA0FFF0110E0
BC1197CA30AA0FC84F7FE62E09FD3D9F
8981D083B3D53B3DF1AC529A70F244C0
3503B1CDE3401EC06030C12A4311F4A5
E33AB4C45C2570B8AD15A921F752DEB6
214BF3C129547AF31D32A5ECB4742192
E01001FF0FAA55FC924D535441FF0700
1F1E1D1C1B1A19180706050403020100 # BCM35230/early MTK5369 and LG1152
7184C9C428D03C445188234D5A827196 # mtk5369 - Mediatek GP4 - HE_DTV_GP4I_AFAAATAA
385A992430196A8C44F1985823C01440 # mtk5398 (a2) - Mediatek NetCast 4/4.5 - HE_LCD_NC5M_AAAAAIAA
8E32E4608871ECE9B6301999D5155A07 # mtka5lr (mt5882) - Mediatek webOS 2 (2015) - HE_DTV_W15L_AFAAABAA
6856A0482475A8B41728A35474810203 # new BCM35230
2F2E2D2C2B2A29281716151413121110 # Saturn7/BCM3556
212D3B2A5C2D3A2C4D5B234B1A321D2A # new Saturn7/old LM1
4F836AAEB4301F26172A9B0E1120EAF4 # LM1 PDP
4EE662C7A2C0917F7328DE73A0836C6B # LM1 LCD
2F534ABE34801A36B7DA6B3EB1C04AD4 # m1 - MStar non-webOS - LN45*, LN53*, LN54*, LN565*, LA61*, LA643*, MA53*, PN45*, PN65*
7B2CA5943D2E752CF58606228C5B2DAD # m1a - MStar non-webOS (L15 signage) - BS_LCD_LE15_AAAAAIAM
D55C6864035A8C8A2B35A6D6C4565596 # m2 - MStar SimpleSmart - HE_LCD_SS1A_AFAAAIAA
ADB92D9E23035522F4708CC259B31EA2 # m2 - MStar webOS 3.0 (2016) - HE_DTV_W16R_AFAAABAA
D2E6EE17639DFE2F81D3840FA0BC334A # m2r - MStar webOS 3.5 (2017) - HE_DTV_W17R_AFAAABAA
4F6DE80C0362FD562464BC2073D15567 # m3 - MStar webOS 4.0 (2018) - HE_DTV_W18R_AFAAABAA
88723D91920712D0BAFE87A25E6E8EC7 # m3r - MStar webOS 4.5 (2019) - HE_DTV_W19R_AFAAATAA
68A284B4953CAD15024BED2C4F852A09 # lm14 - MStar NetCast 4.5 (2014/2015) - HE_LCD_NC5U_AAADABAA
19F51EE9B949C89E41AE136F48BB405C # lm14a - MStar webOS 2 (2015) - HE_DTV_W15A_AFADABAA
F8F6BD1AA24506C2759E1BE1D51BB43C # lm14alite - MStar webOS 2 (2015) - HE_DTV_W15B_AFADABAA
96F464CB29CDFF5441FD87D47D084FF8 # lm15u - MStar webOS 2 (2015) - HE_DTV_W15U_AFADABAA
6FCCC4AA3389B614BABE462498D2020A # lm18a - MStar webOS 4.0 (2018) - HE_DTV_W18A_AFADATAA
806B982279521809DBAD9E2E6BF377763903565A7EB4604BAB1E1503DBFC4326 # lm21a - MStar webOS 6 (2021) - HE_DTV_W21A_AFADATAA
B65119E0E6CB5DB19C69B4CC78FAC3A87C747E5AEFDE8FF58F2CD47128D9E16D # lm21u (mt5889) - MStar webOS 6 (2021) - HE_DTV_W21U_AFADATAA
FC9D81DEC206BA62614C949C43D2DA91D23E9FF3DF9674D69A444D13277BDF96 # lm21ut - MStar webOS 6 StanbyME (2022) - HE_DTV_N21D_AFAAATAA
3435663331313732316538383063663538306161643131653335323334613034 # lm21an - MStar webOS 7 (2022) - HE_DTV_W22A_AFADATAA
1FB2C3B789D5EA48ED16E79A0343986C691DACEC872BB07787D0F722AF5D1E2C # lm21ann - MStar webOS 8 (2023) - HE_DTV_W23A_AFADATAA
4813B5B63C998A2874EF3320684AC8D9 # lg1152 - LX GP4 - HE_DTV_GP4H_AFAAATAA
14B3623488212250C7C992AACD537447 # lg115x - LX NC4 - HE_LCD_NC4H_AAADABAA
12C344FDD2871C983CD0FBBC25143974 # lg1154 (h13, goldfinger) - LX webOS 1 (2014) - HE_DTV_WT1H_AFAAABAA
34CC219D3AFC102433109BBC1DA44095 # m14 (m14tv) - LX webOS 1 (2014) - HE_DTV_WT1M_AFAAATAA
5A167D8C342EF094800E7CFA2D10F2D0 # m14 (m14tv) - LX webOS 2 (2015) - HE_DTV_W15M_AFAAATAA
13F56BE4B4A0829598DB8F74065A263B # h15 - LX webOS 2 (2015) - HE_DTV_W15H_AFADATAA
3679EF1840B7FDEBC1FBF95A0CAFCE3E # m16 - LX webOS 3.0 (2016) - HE_DTV_W16M_AFADABAA
5804DF78CB8DC6A71C05DAB0F1EDE3E1 # m16lite - LX webOS 3.0 (2016) - HE_DTV_W16N_AFADABAA
1B3C76ADD3F5EE6B089DB253747A8CD4 # m16p - LX webOS 3.5 (2017) - HE_DTV_W17H_AFADABAA
3C9D30DF3A95C1AA41928813292BD947 # m16plite - LX webOS 3.5 (2017) - HE_DTV_W17M_AFADATAA
89E11D498392F5A521145738EF036AE5 # m16pstb - LX webOS 3.5 (2017) - HE_DTV_W17S_AFADATAA
437C02F0DF99F2072D1A64EEBBD2953B # m16pp - LX webOS 4.0 (2018) - HE_DTV_W18H_AFADABAA
3471D9BFC5F4B34A8997D56932F34D94 # m16pplite - LX webOS 4.0 (2018) - HE_DTV_W18M_AFADATAA
3FE1CBE11BD658BB37813E05052D5FE5 # m16p3 - LX webOS 4.5 (2019) - HE_DTV_W19H_AFADABAA
A2FA48FCC1A22FD2F1944BEFA8403765EF178D4F4AB0E81AC7B5B267ACBDF14D # m23 - LX webOS 8 (2023) - HE_DTV_W23M_AFADATAA
E529BCDEDF8E49667C0FA3A81174B65E # o18 - LX webOS 4.0 (2018) - HE_DTV_W18O_AFABABAA
4ACD2CA8425BBA6C49FD03A174300239 # o18 - LX webOS 4.5 (2019) - HE_DTV_W19O_AFABABAA
C9EF645424A625BBAE7521394564025EC6252658FB650D33633111BD40C76011 # o20 - LX webOS 5 (2020) - HE_DTV_W20O_AFABATAA
944288798A122C6130B661BEE52DF4FE42120F60A61E312DCFC1411E300A29AE # o20n - LX webOS 6 (2021) - HE_DTV_W21O_AFABATAA
3861333237633238613136633438663239623238623037656335623433353862 # o22 - LX webOS 7 (2022) - HE_DTV_W22O_AFABATAA
CF5D6DC934F18618B968382368E17BA971DEAA2ECDFC906874B327D87076E228 # o22n - LX webOS 8 (2023) - HE_DTV_W23O_AFABATAA
53D6DC79418C1A2371DC9F926CD3A3A06F4E7E4396464B5F41248083C2C65637 # o22n2 - LX webOS 9 (2024) - HE_DTV_W24G_AFABATAA
B7724DBBF2AEA073131E8E7D62D114E2AA02F99D17CD7350C14466624528ED79 # o24 - LX webOS 9 (2024) - HE_DTV_W24O_AFABATAA
FE90B0C1BE8CC28A9738333F95AC2C58777BDE7D4E8CABABA73B24FB7D1781C7 # o22n3 - LX webOS 10 (2025) - HE_DTV_W25G_AFABATAA
52A208FA24E7E70730A40999B1C22C148F4920484BC50B515D243E35D14689F1 # o24n - LX webOS 10 (2025) - HE_DTV_W25O_AFABATAA
CD4171FC9C06869627A67EA7B66D739D
0EE52A12A2EB5DE2E13999187B14913F6D3367A79B39AC35979A51E5C12A4FDF # o208k - LX webOS 5 (2020) - HE_DTV_W20K_AFADATAA
E27E6AFE44B7866D60C24ED27904ECB296CA69B4251478B5248C03851F08ECF5 # e60n - LX webOS 6 (2021) - HE_DTV_W21K_AFADATAA
F2B78AEBAD6D86A17B4742B2B84B60F4
C2FBBC5DDD9D366B7FD6CAEB90F86039
1C966DFA0E5AE9946AAF8D2EC06B9E18
AD17A5923B525FD21DB765A5B6822FBD
388BE4B04BD98E7C3CA45A4C6CA346DD2EB32BDCD05DC28FC4A87C9625294A5E # k6lp - Realtek webOS 5 (2020) - HE_DTV_W20P_AFADATAA
377050F9B9D91CD803ACAACCEA4046DD99B01CFBB0010451F4F87A1620C4BAEF # k6lpfhd - Realtek webOS 5 (2020) - HE_DTV_W20L_AFAAJAAA
395324AD369A529EABAC71FE1E72C25CE25594294D47303BCB2629241AFA4C98 # k6hp - Realtek webOS 5 (2020) - HE_DTV_W20H_AFADABAA
74514676D68B9A72A0093CEF56D3067484E1F4D5CF7D4B4ED389BED030FA1B09 # k7lp - Realtek webOS 6 (2021) - HE_DTV_W21P_AFADATAA
6A42D2485B716B25AE5C9921176588D167C25B902D4EF2903AF5C1FCC61D34C9 # k8lp - Realtek webOS 7 (2022) - HE_DTV_W22P_AFADATAA
A35A57DFDD8266F7CE1AF991EC67BABF6723653ABB9A7D48A4B8AB2A2485BCFE # k8hp - Realtek webOS 7 (2022) - HE_DTV_W22H_AFADATAA
703373367638792F423F4528482B4D6251655468576D5A7134743777217A2443 # k8lp - Realtek webOS 7 hospitality (2022) - HE_IDD_H22P_AHAAATAA
6251655468576D5A7133743677397A24432646294A404E635266556A586E3272 # k8hp - Realtek webOS 7 hospitality (2022) - HE_IDD_H22H_AHAAATAA
3764336361633437326166373639383663353863363039316332383031626637 # k8ap - Realtek webOS 7 (2022) - HE_DTV_W22L_AFAAATAA
DFFD1E4F093E305451D4F3752E63BA9A3E6A6404922D986DF36C00818F5595C1 # k8hpp - Realtek webOS 8 (2023) - HE_DTV_W23H_AFADATAA
EC2C89B4AF45B5EB7EA9A83DD2387810C0815BD31BBFE1D17C809E7D68339112 # k8lpn - Realtek webOS 8 (2023) - HE_DTV_W23P_AFADATAA
F322A9CA1D523C358DD2FD97D5660E25386C9C60E423632AEC9723D282BE971D # kf23f - Realtek webOS 8 smart monitors (2023) - HE_MNT_S23Y_AAAAGLAA
6252A0816884997B2FCA30662561A721A4BCC40B18CBEA5D363FA844F17D7DE9 # kid23q - Realtek webOS 8 ultrawide monitors (2023) HE_MNT_S23Z_AAACGLAA
7638792F423F4528482B4D6251655368566D597133743677397A24432646294A # k8lpn - Realtek webOS 8 hospitality (2023) - HE_IDD_H23P_AHAAATAA
6B5AD1BE81D7A1A494F58EB659431850C1B681826EE4428394D4897052691756 # k8lpn2 - Realtek webOS 9 (2024) - HE_DTV_W24P_AFADATAA
FA9EBB838B7BAFBA75EFE8D5A3560374EB0699A113411CA924051B4ADB52E10D # k24 - Realtek webOS 9 (2024) - HE_DTV_W24H_AFADATAA
1A8ADAB21D9FF995677DB32BCE2E0CD559AE86840EBF4A696872076E37DFFE8F # k24t - Realtek webOS 9 StanbyME 2 (2024) - HE_DTV_N24D_AFADATAA
25DF24166745B52EAB661455BED43DE376320FAA1F7824877B938DB869308B18 # k25lp - Realtek webOS 10 (2025) - HE_DTV_W25P_AFADATAA
B2E8C3E214F044B823916E48FA074E606C7C5CD5E6902B6F99BD903DAC0C792F # k24n - Realtek webOS 10 (2025) - HE_DTV_W25H_AFADATAA
B23981FD3642CDF401E7A0C2FADBDA4399B6AAF9600B802144933B4F4E5855EA # k6lpwee - Realtek webOS 5 (2020) - HE_DTV_C20P_AFADATAA
3263653764623932376235323637653637633035363066353833303235383466 # k8lpwee - Realtek webOS 7 (2022) - HE_DTV_C22P_AFADATAA
46715DFBDE23C2D8CBA7EC4F36BA41AAD28E7EC00FEC51F2843F24654DB09BD3 # k8hpwee - Realtek webOS 7 (2022) - HE_DTV_C22H_AFABATAA
6238323334663232396632613762316537333731333832306664666136333564 # k8apwee - Realtek webOS 7 (2022) - HE_DTV_C22L_AFAAATAA
C43BEC5BAF907800A4DD908628793FCE
F72F0FE303CC80E1BC3406E3B11095E1
A76C33E840C4E776F4568524FA0B9D86
E24424020DAD7B75C0CEDFB6180E4671
1234567890abcdef1234567890abcdef
3503B1CDE3401EC06030C12A4311F4A500000000000000000000000000000000 # Sansui MST135B_10AMVL_M5621_KNIGHT (MBOOTBAK.bin)
+12
View File
@@ -25,6 +25,8 @@ pub struct Format {
} }
pub mod mstar; pub mod mstar;
pub mod mstar_unfd;
pub mod ubi;
pub mod android_ota_zip; pub mod android_ota_zip;
pub mod mstar_secure_old; pub mod mstar_secure_old;
pub mod samsung_old; pub mod samsung_old;
@@ -86,11 +88,21 @@ pub fn get_registry() -> Vec<Format> {
detector_func: crate::formats::android_ota_zip::is_android_ota_zip_file, detector_func: crate::formats::android_ota_zip::is_android_ota_zip_file,
extractor_func: crate::formats::android_ota_zip::extract_android_ota_zip, extractor_func: crate::formats::android_ota_zip::extract_android_ota_zip,
}, },
Format {
name: "mstar_unfd",
detector_func: crate::formats::mstar_unfd::is_mstar_unfd_file,
extractor_func: crate::formats::mstar_unfd::extract_mstar_unfd,
},
Format { Format {
name: "mstar", name: "mstar",
detector_func: crate::formats::mstar::is_mstar_file, detector_func: crate::formats::mstar::is_mstar_file,
extractor_func: crate::formats::mstar::extract_mstar, extractor_func: crate::formats::mstar::extract_mstar,
}, },
Format {
name: "ubi",
detector_func: crate::formats::ubi::is_ubi_file,
extractor_func: crate::formats::ubi::extract_ubi,
},
Format { Format {
name: "samsung_old", name: "samsung_old",
detector_func: crate::formats::samsung_old::is_samsung_old_dir, detector_func: crate::formats::samsung_old::is_samsung_old_dir,
+3
View File
@@ -74,6 +74,9 @@ pub fn extract_funai_mstar(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<()
output_dir: app_ctx.output_dir.join("SoC"), output_dir: app_ctx.output_dir.join("SoC"),
options: app_ctx.options.clone(), options: app_ctx.options.clone(),
dry_run: app_ctx.dry_run, dry_run: app_ctx.dry_run,
lazy_run: app_ctx.lazy_run,
build_prop: app_ctx.build_prop,
dump_keys: app_ctx.dump_keys,
quiet: app_ctx.quiet, quiet: app_ctx.quiet,
verbose: app_ctx.verbose, verbose: app_ctx.verbose,
}; };
+3
View File
@@ -58,6 +58,9 @@ pub fn extract_mstar_secure_old(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Resu
output_dir: app_ctx.output_dir.clone(), output_dir: app_ctx.output_dir.clone(),
options: app_ctx.options.clone(), options: app_ctx.options.clone(),
dry_run: app_ctx.dry_run, dry_run: app_ctx.dry_run,
lazy_run: app_ctx.lazy_run,
build_prop: app_ctx.build_prop,
dump_keys: app_ctx.dump_keys,
quiet: app_ctx.quiet, quiet: app_ctx.quiet,
verbose: app_ctx.verbose, verbose: app_ctx.verbose,
}; };
+457
View File
@@ -0,0 +1,457 @@
use std::any::Any;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
use log::{info, warn};
use crate::{AppContext, InputTarget};
use crate::utils::common;
/// Magic of the MStar UNFD (Universal Nand Flash Driver) Card Information
/// Structure (CIS) that prefixes a raw NAND dump produced by MStar-based
/// tools (common on Toshiba/Winbond/... SLC NAND used in TVs/STBs).
const CIS_MAGIC: &[u8; 16] = b"MSTARSEMIUNFDCIS";
/// Marker that precedes each firmware "bank" inside the dump (a run of
/// `0x02` bytes followed by `0x10 0x10 0x10 0x10`). Used to locate
/// where the real NAND payload begins and how many boot banks are present.
const BANK_MARKER: &[u8] = b"\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x10\x10\x10\x10";
#[derive(Debug)]
pub struct UnfdCisCtx {
/// Raw CIS signature dword right after the magic (e.g. 0x91dc9805).
pub cis_signature: u32,
/// NAND vendor string (e.g. "TOSHIBA").
pub vendor: String,
/// NAND part number string (e.g. "TH58NVG2S3HTA00").
pub part_number: String,
/// Logical page data size in bytes (no OOB included).
pub page_size: u32,
/// Spare/OOB size per page in bytes.
pub spare_size: u32,
/// Number of pages per erase block.
pub pages_per_block: u32,
/// Erase block size in bytes (page_size * pages_per_block).
pub block_size: u32,
/// Total number of erase blocks in the dump.
pub block_count: u32,
/// Total image size in bytes.
pub total_size: u64,
/// Offsets of every `MSTARSEMIUNFDCIS` copy found in the image.
pub cis_copies: Vec<usize>,
/// Offsets of every firmware bank marker found in the image.
pub bank_markers: Vec<usize>,
/// Offset where the actual NAND payload starts (first bank marker).
pub data_start: usize,
}
pub fn is_mstar_unfd_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),
};
// Only the leading CIS header is needed for detection (keep it cheap).
let header = common::read_file(&file, 0, 512)?;
if header.len() < 16 || &header[0..16] != CIS_MAGIC {
return Ok(None);
}
let file_size = file.metadata()?.len();
let cis_signature = u32::from_le_bytes(header[0x10..0x14].try_into().unwrap());
let vendor = cstr(&header[0x40..0x50]);
let part_number = cstr(&header[0x50..0x64]);
let (page_size, spare_size, pages_per_block, block_size, block_count) =
derive_geometry(file_size);
info!(
"- Detected MStar UNFD NAND dump (CIS signature 0x{:08X})",
cis_signature
);
info!(" Vendor: {}, Part: {}", vendor, part_number);
info!(
" NAND geometry: page={}B, spare={}B, pages/block={}, block={}B, blocks={}, total={}B",
page_size, spare_size, pages_per_block, block_size, block_count, file_size
);
Ok(Some(Box::new(UnfdCisCtx {
cis_signature,
vendor,
part_number,
page_size,
spare_size,
pages_per_block,
block_size,
block_count,
total_size: file_size,
cis_copies: Vec::new(),
bank_markers: Vec::new(),
data_start: 0,
})))
}
pub fn extract_mstar_unfd(
app_ctx: &AppContext,
ctx: Box<dyn Any>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut ctx = ctx
.downcast::<UnfdCisCtx>()
.map_err(|_| "Invalid MStar UNFD context")?;
fs::create_dir_all(&app_ctx.output_dir)?;
let input_path = app_ctx
.input_path()
.ok_or("MStar UNFD extractor requires an input path")?;
let mut file = File::open(input_path)?;
// --- Locate CIS copies and firmware bank markers (full scan) ---
ctx.cis_copies = scan_offsets(&mut file, CIS_MAGIC)?;
ctx.bank_markers = scan_offsets(&mut file, BANK_MARKER)?;
ctx.data_start = *ctx.bank_markers.first().unwrap_or(&0);
info!(
" CIS copies: {:?}",
ctx.cis_copies
.iter()
.map(|o| format!("0x{o:X}"))
.collect::<Vec<_>>()
);
info!(
" Bank markers: {:?}",
ctx.bank_markers
.iter()
.map(|o| format!("0x{o:X}"))
.collect::<Vec<_>>()
);
info!(" NAND payload starts at 0x{:X}", ctx.data_start);
// --- Locate the UBI/rootfs region (first UBI EC header) ---
let ubi_start = scan_offsets(&mut file, b"UBI#")?
.into_iter()
.next()
.unwrap_or(ctx.total_size as usize);
// Boot banks are the markers that sit *before* the UBI region.
let mut boot_markers: Vec<usize> = ctx
.bank_markers
.iter()
.cloned()
.filter(|&m| m < ubi_start)
.collect();
boot_markers.sort_unstable();
boot_markers.dedup();
info!(" Boot banks before UBI: {}", boot_markers.len());
// --- Write a human readable CIS info file ---
write_cis_info(&app_ctx.output_dir, &ctx, &boot_markers, ubi_start)?;
// --- Carve each boot bank (marker -> next marker, or UBI start) ---
let mut carved_names = Vec::new();
for (i, &start) in boot_markers.iter().enumerate() {
let end = if i + 1 < boot_markers.len() {
boot_markers[i + 1]
} else {
ubi_start
};
if end <= start {
continue;
}
carve_region(
&mut file,
&app_ctx.output_dir,
&format!("boot_bank{i}"),
start,
end - start,
)?;
carved_names.push(format!("boot_bank{i}"));
}
// --- Carve the UBI/rootfs region (if present) ---
if ubi_start < ctx.total_size as usize {
carve_region(
&mut file,
&app_ctx.output_dir,
"rootfs_ubi",
ubi_start,
ctx.total_size as usize - ubi_start,
)?;
carved_names.push("rootfs_ubi".to_string());
}
// --- Carve the raw NAND payload (everything after the CIS header) ---
let payload_len = ctx.total_size.saturating_sub(ctx.data_start as u64);
if payload_len > 0 {
file.seek(SeekFrom::Start(ctx.data_start as u64))?;
let out_path = Path::new(&app_ctx.output_dir).join("nand_data.bin");
let mut out = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&out_path)?;
let copied = io::copy(&mut file.take(payload_len), &mut out)?;
info!(
" Carved raw NAND payload -> nand_data.bin ({} bytes, 0x{:X}..0x{:X})",
copied,
ctx.data_start,
ctx.data_start as u64 + copied
);
// --- Recursively extract any inner format from the carved payload ---
recurse_extract(&out_path, app_ctx)?;
} else {
warn!(" No NAND payload found after CIS header, skipping carve.");
}
// --- Recurse on the carved boot banks as well ---
for name in &carved_names {
let p = Path::new(&app_ctx.output_dir).join(format!("{name}.bin"));
if p.exists() {
if let Err(e) = recurse_extract(&p, app_ctx) {
warn!(" Recursion on {name} failed: {e}");
}
}
}
Ok(())
}
/// Tries every registered format against the carved `path` and extracts
/// each one that matches. The `mstar` format is intentionally skipped:
/// this raw dump's boot script ships as U-Boot `filepartload` format
/// strings (filled at runtime), so `mstar` would only produce empty
/// stubs. Errors from individual formats are caught so one failure does not
/// abort the rest.
fn recurse_extract(
payload_path: &Path,
parent_ctx: &AppContext,
) -> Result<(), Box<dyn std::error::Error>> {
let file = File::open(payload_path)?;
let sub_dir = parent_ctx.output_dir.join("nand_data_extracted");
fs::create_dir_all(&sub_dir)?;
let in_ctx: AppContext = AppContext {
input: InputTarget::File(file),
input_path: Some(payload_path.to_path_buf()),
output_dir: sub_dir,
options: parent_ctx.options.clone(),
dry_run: parent_ctx.dry_run,
lazy_run: parent_ctx.lazy_run,
build_prop: parent_ctx.build_prop,
dump_keys: parent_ctx.dump_keys,
quiet: parent_ctx.quiet,
verbose: parent_ctx.verbose,
};
info!(" Scanning carved NAND payload for inner formats...");
for format in crate::formats::get_registry() {
if format.name == "mstar" {
continue;
}
match (format.detector_func)(&in_ctx) {
Ok(Some(ctx)) => {
info!(" - inner format detected: {}", format.name);
if let Err(e) = (format.extractor_func)(&in_ctx, ctx) {
warn!(" inner extraction ({}) failed: {}", format.name, e);
}
}
Ok(None) => {}
Err(e) => {
warn!(" inner detection ({}) error: {}", format.name, e);
}
}
}
Ok(())
}
/// Derive the NAND geometry purely from the image size. The image is an
/// exact multiple of the page size (no OOB interleaved), which lets us pick
/// a sensible page/block layout even when the CIS body is not fully decoded.
fn derive_geometry(total: u64) -> (u32, u32, u32, u32, u32) {
let page: u32 = 2048;
let spare: u32 = if total % (page as u64) != 0 { 64 } else { 0 };
let mut ppb: u32 = 64;
for candidate in [64u32, 128, 32, 256, 512] {
if (total as u64) % ((page as u64) * (candidate as u64)) == 0 {
ppb = candidate;
break;
}
}
let block = page * ppb;
let blocks = (total / (block as u64)) as u32;
(page, spare, ppb, block, blocks)
}
/// Returns the offsets of every occurrence of `pattern` within `file`,
/// scanning sequentially in buffered chunks. A small overlap (`keep` bytes
/// from the previous chunk) is carried so a match spanning a chunk
/// boundary is still found; the absolute offset is reconstructed from the
/// known file position of the overlap (`hay_base`).
fn scan_offsets(
file: &mut File,
pattern: &[u8],
) -> Result<Vec<usize>, Box<dyn std::error::Error>> {
if pattern.is_empty() {
return Ok(Vec::new());
}
file.seek(SeekFrom::Start(0))?;
let mut offsets = Vec::new();
let mut buf = vec![0u8; 1 << 20];
let mut tail: Vec<u8> = Vec::new();
let keep = pattern.len() - 1;
let mut base: usize = 0; // file offset of the current chunk's buffer region
loop {
let n = file.read(&mut buf)?;
if n == 0 {
break;
}
let hay_base = base.saturating_sub(keep);
let mut hay = tail;
hay.extend_from_slice(&buf[..n]);
let mut start = 0;
while let Some(rel) = find_sub(&hay[start..], pattern) {
let idx = start + rel;
offsets.push(hay_base + idx);
start = idx + pattern.len();
}
tail = hay.split_off(hay.len().saturating_sub(keep));
base += n;
}
Ok(offsets)
}
/// Naive substring search returning the first relative offset of `needle`.
fn find_sub(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.len() > haystack.len() {
return None;
}
haystack.windows(needle.len()).position(|w| w == needle)
}
/// Carves `[start, start+len)` of `file` into `<name>.bin` in `output_dir`.
fn carve_region(
file: &mut File,
output_dir: &Path,
name: &str,
start: usize,
len: usize,
) -> Result<(), Box<dyn std::error::Error>> {
if len == 0 {
return Ok(());
}
file.seek(SeekFrom::Start(start as u64))?;
let out_path = output_dir.join(format!("{name}.bin"));
let mut out = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&out_path)?;
let copied = io::copy(&mut file.take(len as u64), &mut out)?;
info!(
" Carved {} -> {name}.bin ({} bytes, 0x{:X}..0x{:X})",
name,
copied,
start,
start as u64 + copied
);
Ok(())
}
fn write_cis_info(
output_dir: &Path,
ctx: &UnfdCisCtx,
boot_markers: &[usize],
ubi_start: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let path = output_dir.join("cis_info.txt");
let mut f = File::create(path)?;
writeln!(f, "MStar UNFD NAND dump - CIS information")?;
writeln!(f, "=========================================")?;
writeln!(f, "magic: MSTARSEMIUNFDCIS")?;
writeln!(f, "cis_signature: 0x{:08X}", ctx.cis_signature)?;
writeln!(f, "vendor: {}", ctx.vendor)?;
writeln!(f, "part_number: {}", ctx.part_number)?;
writeln!(f)?;
writeln!(f, "Derived NAND geometry:")?;
writeln!(f, " page_size: {} bytes", ctx.page_size)?;
writeln!(f, " spare_size: {} bytes", ctx.spare_size)?;
writeln!(f, " pages_per_block: {}", ctx.pages_per_block)?;
writeln!(f, " block_size: {} bytes", ctx.block_size)?;
writeln!(f, " block_count: {}", ctx.block_count)?;
writeln!(f, " total_size: {} bytes", ctx.total_size)?;
writeln!(f)?;
writeln!(
f,
"cis_copies: {:?}",
ctx.cis_copies
.iter()
.map(|o| format!("0x{o:X}"))
.collect::<Vec<_>>()
)?;
writeln!(
f,
"bank_markers: {:?}",
ctx.bank_markers
.iter()
.map(|o| format!("0x{o:X}"))
.collect::<Vec<_>>()
)?;
writeln!(f, "ubi_start: 0x{:X}", ubi_start)?;
writeln!(f, "data_start: 0x{:X}", ctx.data_start)?;
writeln!(f)?;
writeln!(f, "Carved partitions:")?;
for (i, &m) in boot_markers.iter().enumerate() {
let end = if i + 1 < boot_markers.len() {
boot_markers[i + 1]
} else {
ubi_start
};
writeln!(
f,
" boot_bank{i}: 0x{:X}..0x{:X} ({} bytes)",
m,
end,
end.saturating_sub(m)
)?;
}
if ubi_start < ctx.total_size as usize {
writeln!(
f,
" rootfs_ubi: 0x{:X}..0x{:X} ({} bytes)",
ubi_start,
ctx.total_size,
ctx.total_size as usize - ubi_start
)?;
}
writeln!(f)?;
writeln!(
f,
"The raw NAND payload (after the CIS header) was carved to nand_data.bin"
)?;
writeln!(
f,
"and any inner firmware formats were extracted to nand_data_extracted/."
)?;
Ok(())
}
/// Decode a NUL-terminated, space-padded ASCII field.
fn cstr(b: &[u8]) -> String {
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
String::from_utf8_lossy(&b[..end]).trim().to_string()
}
+3
View File
@@ -129,6 +129,9 @@ pub fn extract_pfl_upg(app_ctx: &AppContext, _ctx: Box<dyn Any>) -> Result<(), B
output_dir: output_path, output_dir: output_path,
options: app_ctx.options.clone(), options: app_ctx.options.clone(),
dry_run: app_ctx.dry_run, dry_run: app_ctx.dry_run,
lazy_run: app_ctx.lazy_run,
build_prop: app_ctx.build_prop,
dump_keys: app_ctx.dump_keys,
quiet: app_ctx.quiet, quiet: app_ctx.quiet,
verbose: app_ctx.verbose, verbose: app_ctx.verbose,
}; };
+1 -1
View File
@@ -75,7 +75,7 @@ pub fn extract_philips_bdp(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<()
info!("Checking if it's also MTK BDP..."); info!("Checking if it's also MTK BDP...");
let new_file = File::open(&output_path)?; let new_file = File::open(&output_path)?;
let mtk_ctx: AppContext = AppContext { input: InputTarget::File(new_file), input_path: Some(output_path.clone()), output_dir: app_ctx.output_dir.join("0"), options: app_ctx.options.clone(), dry_run: app_ctx.dry_run, quiet: app_ctx.quiet, verbose: app_ctx.verbose }; let mtk_ctx: AppContext = AppContext { input: InputTarget::File(new_file), input_path: Some(output_path.clone()), output_dir: app_ctx.output_dir.join("0"), options: app_ctx.options.clone(), dry_run: app_ctx.dry_run, lazy_run: app_ctx.lazy_run, build_prop: app_ctx.build_prop, dump_keys: app_ctx.dump_keys, quiet: app_ctx.quiet, verbose: app_ctx.verbose };
if let Some(result) = formats::mtk_bdp::is_mtk_bdp_file(&mtk_ctx)? { if let Some(result) = formats::mtk_bdp::is_mtk_bdp_file(&mtk_ctx)? {
info!("- MTK BDP file detected!\n"); info!("- MTK BDP file detected!\n");
+3
View File
@@ -129,6 +129,9 @@ pub fn extract_sony_bdp(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<(), B
output_dir: mtk_extraction_path, output_dir: mtk_extraction_path,
options: app_ctx.options.clone(), options: app_ctx.options.clone(),
dry_run: app_ctx.dry_run, dry_run: app_ctx.dry_run,
lazy_run: app_ctx.lazy_run,
build_prop: app_ctx.build_prop,
dump_keys: app_ctx.dump_keys,
quiet: app_ctx.quiet, quiet: app_ctx.quiet,
verbose: app_ctx.verbose, verbose: app_ctx.verbose,
}; };
+366
View File
@@ -0,0 +1,366 @@
//! UBI / UBIFS NAND rootfs extractor.
//!
//! Handles raw UBI images as commonly carved out of MStar/Toshiba NAND dumps
//! (e.g. `rootfs_ubi.bin` produced by the `mstar_unfd` format). These images
//! frequently still carry the NAND spare/OOB bytes interleaved after every
//! page, and use a vendor-quirked UBI layout (EC header `data_offset` field
//! reads 0). This module:
//!
//! 1. Detects the `UBI#` erase-counter header at offset 0.
//! 2. Auto-detects and strips interleaved OOB (page + spare geometry).
//! 3. Parses per-PEB EC/VID headers (big-endian) and rebuilds each logical
//! volume by ordering LEBs and keeping the copy with the highest sqnum.
//! 4. Reads the UBI volume table (layout volume) to recover volume names.
//! 5. Hands each data volume's reconstructed image to the UBIFS walker
//! ([`ubifs`]) which rebuilds the file tree.
use std::any::Any;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{Read, Seek, SeekFrom};
use log::{info, warn};
use crate::AppContext;
pub mod ubifs;
/// UBI erase-counter header magic ("UBI#").
const EC_MAGIC: &[u8; 4] = b"UBI#";
/// UBI volume-identifier header magic ("UBI!").
const VID_MAGIC: &[u8; 4] = b"UBI!";
/// vol_id of the internal layout volume that holds the volume table.
const UBI_LAYOUT_VOLUME_ID: u32 = 0x7FFF_EFFF;
/// Size of a single volume-table record.
const UBI_VTBL_RECORD_SIZE: usize = 172;
/// NAND main page size we assume for OOB detection.
const NAND_PAGE_SIZE: usize = 2048;
/// Geometry / layout information for a UBI image.
#[derive(Debug, Clone)]
pub struct UbiCtx {
/// Physical erase block size as stored in the file (may include OOB).
pub phys_peb_size: usize,
/// OOB/spare bytes interleaved after each `NAND_PAGE_SIZE` page (0 = none).
pub oob_size: usize,
/// Clean PEB size after OOB has been removed.
pub clean_peb_size: usize,
/// Number of physical erase blocks in the image.
pub peb_count: usize,
}
/// Detect a UBI image: `UBI#` at offset 0, and derive PEB geometry (including
/// any interleaved NAND OOB) from the spacing of consecutive EC headers.
pub fn is_ubi_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 mut head = [0u8; 4];
{
let mut f = file;
f.seek(SeekFrom::Start(0))?;
if f.read_exact(&mut head).is_err() {
return Ok(None);
}
}
if &head != EC_MAGIC {
return Ok(None);
}
let file_size = file.metadata()?.len() as usize;
// Find the spacing between the first two EC headers to learn the physical
// PEB size (OOB-interleaved or not). Scan a bounded prefix so detection
// stays cheap.
let scan_len = file_size.min(4 * 1024 * 1024);
let prefix = crate::utils::common::read_file(&file, 0, scan_len)?;
let phys_peb_size = match second_magic_offset(&prefix, EC_MAGIC) {
Some(d) if d > 0 => d,
// Only one EC header visible in the prefix — fall back to a common
// clean PEB size guess.
_ => guess_single_peb(file_size),
};
let oob_size = detect_oob(phys_peb_size);
let pages = phys_peb_size / (NAND_PAGE_SIZE + oob_size);
let clean_peb_size = pages * NAND_PAGE_SIZE;
let peb_count = file_size / phys_peb_size;
if clean_peb_size == 0 || peb_count == 0 {
return Ok(None);
}
info!("- Detected UBI image");
info!(
" physical PEB: {} bytes, OOB/page: {} bytes, clean PEB: {} bytes, PEBs: {}",
phys_peb_size, oob_size, clean_peb_size, peb_count
);
Ok(Some(Box::new(UbiCtx {
phys_peb_size,
oob_size,
clean_peb_size,
peb_count,
})))
}
/// A reconstructed logical volume: its LEBs concatenated in `lnum` order.
struct Volume {
/// lnum -> (sqnum, leb data)
lebs: HashMap<u32, (u64, Vec<u8>)>,
}
pub fn extract_ubi(
app_ctx: &AppContext,
ctx: Box<dyn Any>,
) -> Result<(), Box<dyn std::error::Error>> {
let ctx = ctx.downcast::<UbiCtx>().map_err(|_| "Invalid UBI context")?;
let input_path = app_ctx
.input_path()
.ok_or("UBI extractor requires an input path")?;
let mut file = File::open(input_path)?;
fs::create_dir_all(&app_ctx.output_dir)?;
// --- Parse every physical erase block ---
let mut volumes: HashMap<u32, Volume> = HashMap::new();
let mut buf = vec![0u8; ctx.phys_peb_size];
for peb in 0..ctx.peb_count {
file.seek(SeekFrom::Start((peb * ctx.phys_peb_size) as u64))?;
if file.read_exact(&mut buf).is_err() {
break;
}
let clean = strip_oob(&buf, ctx.oob_size, ctx.clean_peb_size);
if &clean[0..4] != EC_MAGIC {
continue; // erased / non-UBI block
}
// EC header (big-endian). vid_hdr_offset @0x10, data_offset @0x14.
let mut vid_hdr_offset = be32(&clean, 0x10) as usize;
let mut data_offset = be32(&clean, 0x14) as usize;
// Vendor quirk: fields may read 0. Fall back to page-aligned defaults.
if vid_hdr_offset == 0 || vid_hdr_offset + 64 > ctx.clean_peb_size {
vid_hdr_offset = NAND_PAGE_SIZE;
}
if data_offset == 0 || data_offset >= ctx.clean_peb_size {
data_offset = vid_hdr_offset + NAND_PAGE_SIZE;
}
if vid_hdr_offset + 64 > clean.len() || &clean[vid_hdr_offset..vid_hdr_offset + 4] != VID_MAGIC
{
continue; // no valid VID header -> unmapped PEB
}
// VID header (big-endian). vol_id @0x08, lnum @0x0C, sqnum @0x28.
let vol_id = be32(&clean, vid_hdr_offset + 0x08);
let lnum = be32(&clean, vid_hdr_offset + 0x0C);
let sqnum = be64(&clean, vid_hdr_offset + 0x28);
if data_offset >= clean.len() {
continue;
}
let leb_data = clean[data_offset..].to_vec();
let vol = volumes.entry(vol_id).or_insert_with(|| Volume {
lebs: HashMap::new(),
});
// Keep the newest copy (highest sqnum) of each LEB.
match vol.lebs.get(&lnum) {
Some((prev_sq, _)) if *prev_sq >= sqnum => {}
_ => {
vol.lebs.insert(lnum, (sqnum, leb_data));
}
}
}
if volumes.is_empty() {
warn!(" No valid UBI volumes found");
return Ok(());
}
// --- Recover volume names from the layout volume's volume table ---
let names = volumes
.get(&UBI_LAYOUT_VOLUME_ID)
.map(|v| parse_volume_table(v))
.unwrap_or_default();
info!(" Found {} UBI volume(s)", volumes.len());
// --- Reconstruct and extract each data volume ---
let mut vol_ids: Vec<u32> = volumes.keys().cloned().collect();
vol_ids.sort_unstable();
for vol_id in vol_ids {
if vol_id == UBI_LAYOUT_VOLUME_ID {
continue; // internal layout volume, not a filesystem
}
let vol = &volumes[&vol_id];
let name = names
.get(&vol_id)
.cloned()
.unwrap_or_else(|| format!("vol_{vol_id}"));
let image = reconstruct_volume(vol);
info!(
" Volume '{}' (id {}): {} LEBs, {} bytes",
name,
vol_id,
vol.lebs.len(),
image.len()
);
// Always dump the raw reconstructed volume image alongside the tree.
let img_path = app_ctx.output_dir.join(format!("{name}.ubifs"));
if let Err(e) = fs::write(&img_path, &image) {
warn!(" Could not write {}: {}", img_path.display(), e);
}
// Walk the UBIFS filesystem and rebuild files.
let out_dir = app_ctx.output_dir.join(&name);
match ubifs::extract_ubifs(&image, &out_dir) {
Ok(n) => info!(" Extracted {} file(s) from '{}'", n, name),
Err(e) => warn!(" UBIFS extraction of '{}' failed: {}", name, e),
}
}
Ok(())
}
/// Concatenate a volume's LEBs in ascending `lnum` order.
fn reconstruct_volume(vol: &Volume) -> Vec<u8> {
let mut lnums: Vec<u32> = vol.lebs.keys().cloned().collect();
lnums.sort_unstable();
let mut out = Vec::new();
for lnum in lnums {
out.extend_from_slice(&vol.lebs[&lnum].1);
}
out
}
/// Parse the UBI volume table (stored in the layout volume) into a
/// `vol_id -> name` map. Each record is `UBI_VTBL_RECORD_SIZE` bytes; the
/// record index equals the volume id.
fn parse_volume_table(layout: &Volume) -> HashMap<u32, String> {
let mut names = HashMap::new();
// The layout volume mirrors the table across its LEBs; LEB 0 is enough.
let Some((_, data)) = layout.lebs.get(&0).or_else(|| layout.lebs.values().next()) else {
return names;
};
let count = data.len() / UBI_VTBL_RECORD_SIZE;
for idx in 0..count {
let base = idx * UBI_VTBL_RECORD_SIZE;
let rec = &data[base..base + UBI_VTBL_RECORD_SIZE];
let reserved_pebs = be32(rec, 0x00);
if reserved_pebs == 0 {
continue; // unused record
}
let name_len = be16(rec, 0x0E) as usize;
if name_len == 0 || name_len > 128 {
continue;
}
let name = String::from_utf8_lossy(&rec[0x10..0x10 + name_len]).to_string();
if !name.is_empty() {
names.insert(idx as u32, sanitize_name(&name));
}
}
names
}
/// Remove path separators / control characters from a volume name so it is
/// safe to use as a directory name.
fn sanitize_name(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_control() || c == '/' || c == '\\' || c == ':' {
'_'
} else {
c
}
})
.collect::<String>()
.trim()
.to_string()
}
/// Remove interleaved OOB from a physical PEB, returning the clean main data.
fn strip_oob(peb: &[u8], oob_size: usize, clean_peb_size: usize) -> Vec<u8> {
if oob_size == 0 {
return peb[..clean_peb_size.min(peb.len())].to_vec();
}
let step = NAND_PAGE_SIZE + oob_size;
let mut out = Vec::with_capacity(clean_peb_size);
let mut pos = 0;
while pos + NAND_PAGE_SIZE <= peb.len() && out.len() < clean_peb_size {
out.extend_from_slice(&peb[pos..pos + NAND_PAGE_SIZE]);
pos += step;
}
out.truncate(clean_peb_size);
out
}
/// Offset of the second occurrence of `magic` (i.e. the distance from the
/// first), searched only on page-aligned boundaries for speed.
fn second_magic_offset(data: &[u8], magic: &[u8; 4]) -> Option<usize> {
let mut off = NAND_PAGE_SIZE;
while off + 4 <= data.len() {
if &data[off..off + 4] == magic {
return Some(off);
}
off += NAND_PAGE_SIZE;
}
None
}
/// Determine the OOB size per page given a physical PEB size. Prefers no OOB;
/// otherwise picks the first spare size that divides the PEB into a power-of-two
/// page count.
fn detect_oob(phys_peb_size: usize) -> usize {
for oob in [0usize, 16, 32, 64, 128, 218, 224, 256] {
let step = NAND_PAGE_SIZE + oob;
if phys_peb_size % step != 0 {
continue;
}
let pages = phys_peb_size / step;
if pages.is_power_of_two() && (16..=4096).contains(&pages) {
return oob;
}
}
0
}
/// Fallback PEB size when only one EC header is present.
fn guess_single_peb(file_size: usize) -> usize {
for peb in [131072usize, 262144, 126976, 524288, 65536] {
if file_size % peb == 0 {
return peb;
}
}
131072
}
// --- endian helpers ---------------------------------------------------------
fn be32(b: &[u8], off: usize) -> u32 {
u32::from_be_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
fn be16(b: &[u8], off: usize) -> u16 {
u16::from_be_bytes([b[off], b[off + 1]])
}
fn be64(b: &[u8], off: usize) -> u64 {
u64::from_be_bytes([
b[off], b[off + 1], b[off + 2], b[off + 3], b[off + 4], b[off + 5], b[off + 6], b[off + 7],
])
}
+348
View File
@@ -0,0 +1,348 @@
//! Minimal UBIFS reader.
//!
//! Rather than walking the on-flash B-tree index (which vendor images often
//! corrupt or lay out non-standardly), this performs a tolerant *linear* scan
//! of every UBIFS node in a reconstructed volume image. It collects inode,
//! directory-entry and data nodes, validates each with its CRC-32, then
//! rebuilds the directory tree and writes out regular files, directories and
//! symlinks.
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{Seek, Write};
use std::path::{Path, PathBuf};
use flate2::Crc;
/// UBIFS common node header magic (little-endian on disk: 31 18 10 06).
const UBIFS_NODE_MAGIC: u32 = 0x0610_1831;
// Node types.
const INO_NODE: u8 = 0;
const DATA_NODE: u8 = 1;
const DENT_NODE: u8 = 2;
// Header / node sizes.
const CH_SZ: usize = 24; // common header
const DATA_NODE_SZ: usize = 48; // header + key + size + compr fields
const KEY_OFF: usize = 24;
// UBIFS logical data block size (uncompressed).
const UBIFS_BLOCK_SIZE: u64 = 4096;
// Compression types.
const COMPR_NONE: u16 = 0;
const COMPR_LZO: u16 = 1;
const COMPR_ZLIB: u16 = 2;
const COMPR_ZSTD: u16 = 3;
// Inode item types stored in directory entries.
const ITYPE_REG: u8 = 0;
const ITYPE_DIR: u8 = 1;
const ITYPE_LNK: u8 = 2;
/// Root inode number in UBIFS.
const ROOT_INO: u32 = 1;
struct Inode {
size: u64,
/// Inline data (symlink target for links).
inline: Vec<u8>,
}
struct Dent {
parent: u32,
target: u32,
itype: u8,
name: String,
}
/// Extract a reconstructed UBIFS volume image into `out_dir`. Returns the
/// number of filesystem objects created.
pub fn extract_ubifs(image: &[u8], out_dir: &Path) -> Result<usize, Box<dyn std::error::Error>> {
let mut inodes: HashMap<u32, Inode> = HashMap::new();
let mut dents: Vec<Dent> = Vec::new();
// inode -> block_no -> decompressed bytes
let mut data: HashMap<u32, HashMap<u64, Vec<u8>>> = HashMap::new();
scan_nodes(image, &mut inodes, &mut dents, &mut data);
if inodes.is_empty() && dents.is_empty() {
return Err("no UBIFS nodes found".into());
}
// Build parent -> children map.
let mut children: HashMap<u32, Vec<&Dent>> = HashMap::new();
for d in &dents {
children.entry(d.parent).or_default().push(d);
}
fs::create_dir_all(out_dir)?;
let mut count = 0usize;
let mut stack: Vec<(u32, PathBuf)> = vec![(ROOT_INO, out_dir.to_path_buf())];
let mut visited: HashMap<u32, ()> = HashMap::new();
while let Some((ino, dir_path)) = stack.pop() {
if visited.insert(ino, ()).is_some() {
continue; // guard against cycles
}
let Some(kids) = children.get(&ino) else {
continue;
};
for d in kids {
let child_path = dir_path.join(sanitize_component(&d.name));
match d.itype {
ITYPE_DIR => {
if fs::create_dir_all(&child_path).is_ok() {
count += 1;
stack.push((d.target, child_path));
}
}
ITYPE_REG => {
if write_regular_file(&child_path, d.target, &inodes, &data) {
count += 1;
}
}
ITYPE_LNK => {
if let Some(ino) = inodes.get(&d.target) {
let target = String::from_utf8_lossy(&ino.inline).to_string();
// Portable: record symlinks as text files (Windows-safe).
if fs::write(&child_path, format!("SYMLINK -> {target}")).is_ok() {
count += 1;
}
}
}
_ => {
// Special files (dev/fifo/sock): note as empty placeholder.
let _ = File::create(&child_path);
count += 1;
}
}
}
}
Ok(count)
}
/// Assemble and write a regular file from its data blocks, truncated to the
/// inode size.
fn write_regular_file(
path: &Path,
ino: u32,
inodes: &HashMap<u32, Inode>,
data: &HashMap<u32, HashMap<u64, Vec<u8>>>,
) -> bool {
let size = inodes.get(&ino).map(|i| i.size).unwrap_or(0);
let mut out = match File::create(path) {
Ok(f) => f,
Err(_) => return false,
};
if let Some(blocks) = data.get(&ino) {
let mut block_nos: Vec<u64> = blocks.keys().cloned().collect();
block_nos.sort_unstable();
for bn in block_nos {
let expected_off = bn * UBIFS_BLOCK_SIZE;
// Pad holes with zeros to keep byte offsets correct.
let cur = out.stream_position().unwrap_or(0);
if expected_off > cur {
let hole = (expected_off - cur) as usize;
let _ = out.write_all(&vec![0u8; hole]);
}
let _ = out.write_all(&blocks[&bn]);
}
}
// Truncate to the exact inode size when known.
if size > 0 {
let _ = out.flush();
if let Ok(f) = File::options().write(true).open(path) {
let _ = f.set_len(size);
}
}
true
}
/// Linear scan of the volume image collecting UBIFS nodes, validated by CRC-32.
fn scan_nodes(
image: &[u8],
inodes: &mut HashMap<u32, Inode>,
dents: &mut Vec<Dent>,
data: &mut HashMap<u32, HashMap<u64, Vec<u8>>>,
) {
let mut off = 0usize;
let n = image.len();
while off + CH_SZ <= n {
// UBIFS nodes are 8-byte aligned.
if le32(image, off) != UBIFS_NODE_MAGIC {
off += 8;
continue;
}
let crc = le32(image, off + 4);
let len = le32(image, off + 8) as usize;
let node_type = image[off + 12];
if len < CH_SZ || off + len > n {
off += 8;
continue;
}
// Validate CRC-32 over everything after the crc field.
let mut c = Crc::new();
c.update(&image[off + 8..off + len]);
if c.sum() != crc {
off += 8;
continue;
}
let node = &image[off..off + len];
match node_type {
INO_NODE => parse_ino(node, inodes),
DENT_NODE => parse_dent(node, dents),
DATA_NODE => parse_data(node, data),
_ => {}
}
// Advance to the next 8-byte-aligned node.
off += (len + 7) & !7;
}
}
fn parse_ino(node: &[u8], inodes: &mut HashMap<u32, Inode>) {
if node.len() < 160 {
return;
}
let ino = le32(node, KEY_OFF); // key[0] = inode number
let size = le64(node, 48);
let mode = le32(node, 104);
let data_len = le32(node, 112) as usize;
let itype = mode_to_itype(mode);
let mut inline = Vec::new();
if itype == ITYPE_LNK && data_len > 0 && 160 + data_len <= node.len() {
inline = node[160..160 + data_len].to_vec();
}
inodes.insert(ino, Inode { size, inline });
}
fn parse_dent(node: &[u8], dents: &mut Vec<Dent>) {
if node.len() < 56 {
return;
}
let parent = le32(node, KEY_OFF); // key[0] = parent inode
let target = le32(node, 40); // low 32 bits of __le64 inum
let itype = node[49];
let nlen = le16(node, 50) as usize;
if 56 + nlen > node.len() {
return;
}
let name = String::from_utf8_lossy(&node[56..56 + nlen]).to_string();
if name.is_empty() {
return;
}
dents.push(Dent {
parent,
target,
itype,
name,
});
}
fn parse_data(node: &[u8], data: &mut HashMap<u32, HashMap<u64, Vec<u8>>>) {
if node.len() < DATA_NODE_SZ {
return;
}
let ino = le32(node, KEY_OFF); // key[0] = inode number
let block_no = (le32(node, KEY_OFF + 4) & 0x1FFF_FFFF) as u64; // key[1] low bits
let out_len = le32(node, 40) as usize;
let compr = le16(node, 44);
let payload = &node[DATA_NODE_SZ..];
let decompressed = match decompress_block(payload, compr, out_len) {
Some(d) => d,
None => return,
};
data.entry(ino).or_default().insert(block_no, decompressed);
}
/// Decompress a single UBIFS data block payload.
fn decompress_block(payload: &[u8], compr: u16, out_len: usize) -> Option<Vec<u8>> {
match compr {
COMPR_NONE => {
let take = out_len.min(payload.len());
Some(payload[..take].to_vec())
}
COMPR_LZO => match minilzo_rs::LZO::init() {
Ok(lzo) => lzo.decompress(payload, out_len).ok(),
Err(_) => None,
},
COMPR_ZLIB => inflate_raw(payload, out_len),
COMPR_ZSTD => crate::utils::compression::decompress_zstd(payload).ok(),
_ => None,
}
}
/// Raw DEFLATE inflate (UBIFS uses headerless zlib streams).
fn inflate_raw(payload: &[u8], out_len: usize) -> Option<Vec<u8>> {
use flate2::Decompress;
use flate2::FlushDecompress;
let mut d = Decompress::new(false); // false = raw deflate (no zlib header)
let mut out = Vec::with_capacity(out_len.max(payload.len()));
match d.decompress_vec(payload, &mut out, FlushDecompress::Finish) {
Ok(_) => Some(out),
Err(_) => {
// Retry assuming a zlib header just in case.
crate::utils::compression::decompress_zlib(payload).ok()
}
}
}
fn mode_to_itype(mode: u32) -> u8 {
match mode & 0o170000 {
0o040000 => ITYPE_DIR,
0o120000 => ITYPE_LNK,
0o100000 => ITYPE_REG,
_ => 0xFF,
}
}
/// Make a single path component safe on the host filesystem.
fn sanitize_component(name: &str) -> String {
let cleaned: String = name
.chars()
.map(|c| {
if c.is_control() || matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|') {
'_'
} else {
c
}
})
.collect();
match cleaned.trim() {
"" | "." | ".." => "_".to_string(),
other => other.to_string(),
}
}
// --- little-endian helpers --------------------------------------------------
fn le16(b: &[u8], off: usize) -> u16 {
u16::from_le_bytes([b[off], b[off + 1]])
}
fn le32(b: &[u8], off: usize) -> u32 {
u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
fn le64(b: &[u8], off: usize) -> u64 {
u64::from_le_bytes([
b[off], b[off + 1], b[off + 2], b[off + 3], b[off + 4], b[off + 5], b[off + 6], b[off + 7],
])
}
+108 -76
View File
@@ -14,37 +14,66 @@ use crate::error::UnixtractError;
#[command( #[command(
name = "unixtract", name = "unixtract",
version, version,
about = "Firmware extractor for various file formats", about = "Firmware package analyzer and extractor",
long_about = "unixtract analyzes and extracts firmware package formats commonly found \ long_about = "unixtract analyzes and extracts firmware package formats commonly found in\n\
in TVs, Blu-Ray players, and AV devices. It supports 35+ different formats \ TVs, Blu-ray players, and AV devices. It supports 35+ formats with built-in\n\
with built-in decryption and decompression capabilities." decryption and decompression capabilities.",
after_help = "\
EXAMPLES:\n\
\n\
# Single file analysis\n\
unixtract --file-input firmware.bin\n\
unixtract --file-input firmware.bin --output output/\n\
unixtract --file-input firmware.bin --lazy-run\n\
unixtract --file-input firmware.bin --build-prop\n\
unixtract --file-input firmware.bin --dump-keys\n\
unixtract --file-input firmware.bin --verbose\n\
\n\
# Bulk / directory processing\n\
unixtract --dir-input firmware_older/\n\
unixtract --dir-input firmware_older/ --output bulk_output/\n\
unixtract --dir-input firmware_older/ --lazy-run\n\
unixtract --dir-input firmware_older/ --build-prop\n\
unixtract --dir-input firmware_older/ --dump-keys\n\
unixtract --dir-input firmware_older/ --output bulk_output/ --verbose\n\
\n\
# Combined usage\n\
unixtract --file-input firmware.bin --lazy-run --build-prop --verbose\n\
unixtract --dir-input firmware_older/ --output extracted_fw/ --verbose --build-prop\n\
unixtract --file-input firmware.bin --dump-keys\n\
unixtract --file-input firmware.bin --lazy-run --dump-keys"
)] )]
struct Args { struct Args {
/// The target file or directory to analyze/extract /// Single firmware binary input
#[arg(required_unless_present = "list_formats")] #[arg(long = "file-input")]
input_target: Option<String>, file_input: Option<String>,
/// Folder to save extracted files to (default: _<INPUT_TARGET>) /// Directory containing multiple firmware binaries for bulk processing
output_directory: Option<String>, #[arg(long = "dir-input")]
dir_input: Option<String>,
/// Format-specific or global options (can be used multiple times) /// Output path for extracted data (default: _<INPUT>)
#[arg(short, long)] #[arg(long = "output")]
options: Vec<String>, output: Option<String>,
/// Enable lazy-run mode — minimal processing for faster analysis
#[arg(long = "lazy-run")]
lazy_run: bool,
/// Extract and display firmware build properties and metadata
#[arg(long = "build-prop")]
build_prop: bool,
/// Dump built-in decryption keys
#[arg(long = "dump-keys")]
dump_keys: bool,
/// List supported formats and exit /// List supported formats and exit
#[arg(long)] #[arg(long = "list-formats")]
list_formats: bool, list_formats: bool,
/// Only detect the format, do not extract /// Increase verbosity level (repeat for more)
#[arg(long)] #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
dry_run: bool,
/// Quiet mode — suppress non-error output
#[arg(short, long)]
quiet: bool,
/// Verbose mode — show detailed progress information (use -vv for trace)
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8, verbose: u8,
} }
@@ -59,6 +88,9 @@ pub struct AppContext {
pub output_dir: PathBuf, pub output_dir: PathBuf,
pub options: Vec<String>, pub options: Vec<String>,
pub dry_run: bool, pub dry_run: bool,
pub lazy_run: bool,
pub build_prop: bool,
pub dump_keys: bool,
pub quiet: bool, pub quiet: bool,
pub verbose: u8, pub verbose: u8,
} }
@@ -96,25 +128,33 @@ fn clear_terminal() {
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse(); let args = Args::parse();
// Clear the terminal on execution // --- Handle --list-formats (doesn't require input) ---
if !args.quiet { if args.list_formats {
clear_terminal(); let formats = get_registry();
eprintln!("Supported formats ({} total):", formats.len());
for (i, fmt) in formats.iter().enumerate() {
eprintln!(" {:2}. {}", i + 1, fmt.name);
}
return Ok(());
} }
// Initialize logger based on verbosity level // --- Validate mutually exclusive input modes ---
// Default (no flags): show info-level output (normal progress) let (target_path, is_file_mode) = match (&args.file_input, &args.dir_input) {
// -q: errors only (Some(f), None) => (PathBuf::from(f), true),
// -v: debug level (more details) (None, Some(d)) => (PathBuf::from(d), false),
// -vv: trace level (everything) (Some(_), Some(_)) => {
// -vvv: trace level (everything) return Err("Cannot specify both --file-input and --dir-input at the same time".into());
let log_level = if args.quiet {
"error"
} else {
match args.verbose {
0 => "info",
1 => "debug",
_ => "trace",
} }
(None, None) => {
return Err("Either --file-input or --dir-input must be provided".into());
}
};
// --- Logger initialization ---
let log_level = match args.verbose {
0 => "info",
1 => "debug",
_ => "trace",
}; };
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level)) env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level))
@@ -132,34 +172,25 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}) })
.init(); .init();
// Handle --list-formats clear_terminal();
if args.list_formats {
let formats = get_registry();
eprintln!("Supported formats ({} total):", formats.len());
for (i, fmt) in formats.iter().enumerate() {
eprintln!(" {:2}. {}", i + 1, fmt.name);
}
return Ok(());
}
log::info!("unixtract v{}", env!("CARGO_PKG_VERSION")); log::info!("unixtract v{}", env!("CARGO_PKG_VERSION"));
let target_path_str = args.input_target.as_deref().unwrap_or(""); // --- Output directory ---
let target_path = PathBuf::from(target_path_str); let output_path_str = if let Some(ref out) = args.output {
let output_path_str = if let Some(ref out) = args.output_directory {
out.clone() out.clone()
} else { } else {
format!("_{}", target_path.file_name() let fname = target_path.file_name()
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
.ok_or_else(|| UnixtractError::Other("Invalid input file name".to_string()))?) .ok_or_else(|| UnixtractError::Other("Invalid input file name".to_string()))?;
format!("_{}", fname)
}; };
let output_directory_path = PathBuf::from(&output_path_str); let output_directory_path = PathBuf::from(&output_path_str);
if output_directory_path.exists() { if output_directory_path.exists() {
if output_directory_path.is_dir() { if output_directory_path.is_dir() {
let is_empty = fs::read_dir(&output_directory_path)?.next().is_none(); let is_empty = fs::read_dir(&output_directory_path)?.next().is_none();
if !is_empty && !args.quiet { if !is_empty {
log::warn!("Output folder already exists and is NOT empty! Files may be overwritten."); log::warn!("Output folder already exists and is NOT empty! Files may be overwritten.");
eprintln!("Press Enter if you want to continue..."); eprintln!("Press Enter if you want to continue...");
io::stdin().read_line(&mut String::new())?; io::stdin().read_line(&mut String::new())?;
@@ -167,33 +198,35 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
} }
// === DEFAULT MODE (original behavior) === // --- Build AppContext ---
let app_ctx; let app_ctx = if is_file_mode {
if target_path.is_file() {
let file = File::open(&target_path)?; let file = File::open(&target_path)?;
app_ctx = AppContext { AppContext {
input: InputTarget::File(file), input: InputTarget::File(file),
input_path: Some(target_path.clone()), input_path: Some(target_path.clone()),
output_dir: output_directory_path, output_dir: output_directory_path,
options: args.options, options: Vec::new(),
dry_run: args.dry_run, dry_run: args.lazy_run,
quiet: args.quiet, lazy_run: args.lazy_run,
build_prop: args.build_prop,
dump_keys: args.dump_keys,
quiet: args.verbose == 0,
verbose: args.verbose, verbose: args.verbose,
}; }
} else if target_path.is_dir() { } else {
app_ctx = AppContext { AppContext {
input: InputTarget::Directory(target_path.clone()), input: InputTarget::Directory(target_path.clone()),
input_path: Some(target_path.clone()), input_path: Some(target_path.clone()),
output_dir: output_directory_path, output_dir: output_directory_path,
options: args.options, options: Vec::new(),
dry_run: args.dry_run, dry_run: args.lazy_run,
quiet: args.quiet, lazy_run: args.lazy_run,
build_prop: args.build_prop,
dump_keys: args.dump_keys,
quiet: args.verbose == 0,
verbose: args.verbose, verbose: args.verbose,
}; }
} else { };
return Err("Invalid input path!".into());
}
let formats: Vec<Format> = get_registry(); let formats: Vec<Format> = get_registry();
@@ -201,8 +234,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Some(ctx) = (format.detector_func)(&app_ctx)? { if let Some(ctx) = (format.detector_func)(&app_ctx)? {
log::info!("\n{} detected!", format.name); log::info!("\n{} detected!", format.name);
if app_ctx.dry_run { if app_ctx.lazy_run {
log::info!("Dry run — skipping extraction."); log::info!("Lazy-run — skipping extraction.");
return Ok(()); return Ok(());
} }
@@ -213,7 +246,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
(format.extractor_func)(&app_ctx, ctx)?; (format.extractor_func)(&app_ctx, ctx)?;
// Extractor returned with no error
log::info!("\nExtraction finished! Saved extracted files to {}", output_path_str); log::info!("\nExtraction finished! Saved extracted files to {}", output_path_str);
return Ok(()); return Ok(());
} }
+658
View File
File diff suppressed because one or more lines are too long
+3070
View File
File diff suppressed because it is too large Load Diff