Golang toolkit for Panasonic DP-UB9000 (and similar) UHD Blu-ray player research and modification.
- Firmware Tools - Decode, encode, and analyze Panasonic firmware files (PANAEUSB.FRM)
- Cramfs Tools - Extract and create cramfs filesystem images
- Romfs Tools - Extract and create romfs filesystem images
- Device Discovery - Find Panasonic players on the network via SSDP
- USB Disk Management - List, format, and write disk images to USB drives
- TUI Application - Graphical terminal interface for device management
git clone https://github.com/tridentsx/panago.git
cd panago
make buildDownload from the Releases page.
The project builds two binaries:
panago- TUI (terminal UI) for interactive device managementpanago-cli- Command-line interface with all tools as subcommands
panago-cli [command] [subcommand] [options]Extract everything, modify files, rebuild in three steps:
# 1. Extract firmware and all filesystems
panago-cli extract PANAEUSB.FRM ./workspace/
# 2. Modify files in the workspace
# ./workspace/fma5/ = root filesystem (cramfs)
# ./workspace/fma6/ = data partition (romfs)
# ./workspace/fma7/ = app filesystem (cramfs)
# 3. Rebuild and repack firmware
panago-cli build ./workspace/ PANAEUSB_modified.FRM PANAEUSB.FRMWorkspace structure after extraction:
workspace/
├── PROG_0.00.bin (boot program)
├── MINI_7.74.bin (mini partition)
├── DRV1_D110.bin (driver partition 1)
├── DRV1_V304.bin (driver partition 2)
├── BUCD_000.bin (BD certification data)
├── MAIN_metadata.json (MAIN encoding parameters - do not edit)
├── fma4.bin (kernel - kept as binary)
├── fma5/ (root filesystem - editable)
│ ├── sbin/init
│ ├── etc/
│ └── ...
├── fma6/ (data partition - editable)
│ ├── local/fonts/
│ └── ...
└── fma7/ (app filesystem - editable)
├── bin/
└── ...
panago-cli firmware info PANAEUSB.FRMFirmware: PANAEUSB.FRM
Size: 184193072 bytes
Partitions: 6
Name Version Offset Size
-------- -------- ------------ ------------
PROG 0.00 8192 327680
MAIN 3820 335872 180688644
MINI 7.74 181026816 524288
DRV1 D110 181551104 1122688
DRV1 V304 182677504 1122688
BUCD 000 183803904 389120
Note: MAIN's declared size here is itself a few dozen bytes short of the true partition boundary — see Fields That Lie below.
panago-cli firmware decode PANAEUSB.FRM ./extracted/Extracts all partitions:
PROG_0.00.bin- Boot programMAIN.bin- Main data (concatenated kernel + filesystems)MAIN_metadata.json- MAIN encoding parameters (required for re-encoding)MINI_7.74.bin- Mini partitionDRV1_D110.bin,DRV1_V304.bin- Driver partitionsBUCD_000.bin- BD certification data
panago-cli firmware encode ./modified/ output.FRM original.FRMUses the original firmware as a structural template. Non-MAIN partitions must match
the naming convention NAME_VERSION.bin and the exact original size. The MAIN
partition is re-encoded using MAIN.bin + MAIN_metadata.json.
panago-cli firmware split-main MAIN.bin ./main_parts/Splitting MAIN.bin...
fma4: offset=0x0, size=6553600 (raw)
fma5: offset=0x640000, size=47710208 (cramfs)
fma6: offset=0x33c0000, size=18874368 (romfs)
fma7: offset=0x45c0000, size=119275520 (cramfs)
These sizes are the real, fixed NAND partition allocations — each notably larger than the filesystem's actual content (e.g. fma7 is 113.75 MiB allocated vs. ~113.6 MB actually used). See Byte-Exact Reproduction below.
Sub-images:
fma4.bin- Kernel (raw binary)fma5.bin- Root filesystem (cramfs)fma6.bin- Data partition (romfs)fma7.bin- Application filesystem (cramfs)
panago-cli firmware combine-main ./main_parts/ MAIN_modified.binConcatenates fma4.bin through fma7.bin in order.
panago-cli firmware testVerifies Feistel cipher, AES-128-CBC, and LZSS compression round-trips.
panago-cli cramfs list fma5.bind0755 0 usr
d0755 0 tmp
l0777 1 armv4t -> .
d0755 296 sbin
-0755 48060 sbin/init
l0777 3 var -> tmp
...
panago-cli cramfs extract fma5.bin ./rootfs/panago-cli cramfs create ./rootfs/ new_fma5.binpanago-cli cramfs info fma5.binCramfs image: fma5.bin
Files: 171
Directories: 78
Symlinks: 156
Total uncompressed size: 38651830 bytes
panago-cli romfs list fma6.binpanago-cli romfs extract fma6.bin ./data/panago-cli romfs create ./data/ new_fma6.binpanago-cli romfs info fma6.binRomfs image: fma6.bin
Volume name: rom 665d5f54
Image size: 18641888 bytes
Files: 666
Directories: 36
Symlinks: 35
Total content size: 18601986 bytes
# Discover all UPnP devices on the network
panago-cli discover
# Show only Panasonic players
panago-cli discover --panasonic# List available USB disks
panago-cli disk list
# Write a disk image to a USB device
panago-cli disk write /dev/sdX drive.img.gz./bin/panagoThe TUI scans for Panasonic players on startup via SSDP, with manual IP entry as fallback. Features include device shell access, FPC key extraction, backup, USB disk creation, and firmware updates.
Panasonic firmware uses a two-layer encryption scheme applied in sequence:
- AES-128-CBC — outer layer, applied to the entire file
- Custom Feistel cipher — 16-round, 8-byte block cipher with a custom S-box
- Applied to the 48-byte file header and the 8 KB module header block
- Applied to the first and last 5 KB (or 10 KB for large firmwares) of each MAIN sub-entry
File (AES-CBC encrypted)
└── 0x00: File header (48 bytes, Feistel encrypted)
[0:4] = 0x30 (self-size)
[4:8] = payload size (file_size - 48)
[32:44] = "PANASONIC\0\0\0" (product identifier)
[44:48] = Unix timestamp (firmware build date)
└── 0x30: Module header block (8 KB, Feistel encrypted)
Entry 0: "$PaT" marker
Entry 1..N: partition descriptors (48 bytes each)
[0:4] Name (e.g. "MAIN", "PROG")
[4:8] Version (e.g. "3820", "0.00")
[12:16] Offset (partition start in file)
[24:26] TypeFlags (low byte = 0x4D; high: 0=standard, 1=system)
[32:36] Size (partition size in bytes)
[36:40] DataCk (Adler32 of Feistel-decrypted partition data)
[40:44] RingBufSize (0 for most; 0x02000000 for MAIN)
[44:48] EntryCk (Adler32 of entry bytes [0:44])
└── Partitions: PROG, MAIN, MINI, DRV1, DRV1, BUCD
The MAIN partition contains a sub-entry list with individually compressed chunks:
MAIN partition
└── 0x00: First header (48 bytes, Feistel encrypted)
└── 0x30: List header (20 bytes, plaintext)
[0:4] Checksum (Adler32 of bytes [4:] — list header tail + entry records)
[4:8] FormatVersion (always 1)
[8:12] ListSize (total list header + entry records size)
[12:16] DecompSize (PER-CHUNK decompressed size, e.g. 0x1000000 = 16MB —
NOT the total; every chunk but the last is exactly
this size, the last is whatever remains)
[16:20] CompType (2 = LZSS, 0 = uncompressed)
└── Entry records (8 bytes each):
[0:4] Size (entry blob size)
[4:8] Checksum (Adler32 of Feistel-encrypted entry blob)
└── Entry blobs (Feistel encrypted at boundaries):
Each entry:
[0:14] Signature (e.g. "EXTRHEADDRVD ")
[14:16] CompType (2 = raw LZSS)
[16:20] DecompSize (this entry's own decompressed size)
[20:24] DestAddr (load address, 0)
[24:28] CompSize (compressed data size)
[28:32] Slack (BufferConstant - FooterOffset)
[32:36] FooterOffset (64 + align4(CompSize))
[36:40] BaseAddr (0)
[40:44] HdrChecksum (Adler32 of every 16th byte of decompressed data)
[44] ChecksumFlag (0x10)
[64:] LZSS compressed data
[FooterOffset:] "EXTRFOOT" (8 bytes)
BufferConstant is not the same for every entry — it scales with that
entry's own DecompSize. In every real firmware examined, all full
(chunk-size) entries share one value, but the last (partial-size) entry uses
a proportionally smaller one. panago captures each entry's real
BufferConstant alongside its DecompSize during extraction and looks it up
by size when re-encoding, rather than assuming one constant fits all — this
matters as soon as modding changes which chunk sizes exist. The exact
formula relating DecompSize to BufferConstant isn't confirmed (only two
real data points exist so far); for a genuinely new chunk size not seen in
the source firmware, panago falls back to a deliberately generous estimate
and prints a warning. A second real firmware with a different total content
size (different last-chunk size) would supply a third data point and might
be enough to pin down the real formula — see pkg/firmware/encode_main.go's
estimateBufferConstant.
The MAIN sub-entries use Haruhiko Okumura's classic public-domain LZSS
reference encoder (the binary-tree-based match finder, not a hash-chain
heuristic), adapted only in that the ring buffer is zero-filled instead of
space-filled. pkg/firmware/lzss.go's CompressLZSS is a direct port of
that reference algorithm and reproduces the original firmware's compressed
bytes exactly (verified against every MAIN sub-entry in a real firmware
image, including the odd-sized last chunk):
- 4096-byte ring buffer, initialized to
0x00 - Initial write position:
0xFEE(4078) - Offsets: 12 bits; lengths: 4 bits with bias +3 (range 3–18 bytes)
- Flag byte precedes each group of 8 tokens:
1= literal,0= back-reference - Back-references into the pre-filled zero window (offsets
0x000–0xFED) are valid for the first 4096 output bytes, via the reference encoder's normal tree search — no special-casing needed once the exact algorithm is used
Every checksum in this format is Adler32 — there is no plain word-sum anywhere, despite what field names might suggest.
| Location | Algorithm |
|---|---|
List header checksum (list_header[0:4]) |
Adler32 of list header bytes [4:] + all entry records |
List entry checksum (entry_record[4:8]) |
Adler32 of Feistel-encrypted entry blob |
Entry header checksum (entry_hdr[40:44]) |
Adler32 of every 16th byte of decompressed chunk data |
Module entry DataCk (mod_entry[36:40]) |
Adler32 of Feistel-decrypted partition data (non-MAIN only) |
Module entry EntryCk (mod_entry[44:48]) |
Adler32 of module entry bytes [0:44] |
Panasonic uses "old cramfs format" (flags=0, no FSID_VERSION_2):
- 4 KB block size
- Real zlib (not Go's
compress/flate, which is a different DEFLATE implementation and cannot reproduce these exact bytes) atlevel=6, windowBits=14, memLevel=7, Z_DEFAULT_STRATEGY, producing a standard zlib-wrapped stream (2-byte header + Adler32 trailer) — not raw deflate.pkg/cramfs/zdeflate.gois a pure-Go port of zlib 1.2.12'sdeflate_slow+ Huffman encoder at exactly these parameters. - Symlinks stored as compressed data (same as regular files)
- Superblock UID/GID are a fixed placeholder (
41605/100) on every inode, not real ownership; the "total size" field is also a fixed placeholder (0x00010000), not the real image size - Directory listing order is the original build machine's
readdir()order, not alphabetical —panagocaptures this at extraction time (a sidecar<dir>_order.json) and replays it on rebuild - Layout is two full passes over the tree, not one: every directory's own listing is written first (a complete depth-first pass touching every directory, no file data at all), then a second full pass writes every file's/symlink's data — so a directory many levels deep can appear at a tiny offset while a shallow sibling's data lands near the end of the image
- Identical file/symlink content is stored once and shared across the whole
image (not just within one directory) — common for
libfoo.so/libfoo.so.N-style duplicate symlinks
Panasonic's romfs partition (fma6) is standard Linux romfs — confirmed
against the actual kernel source in Panasonic's own GPL disclosure
(fs/romfs/* is unmodified upstream) — built with
genromfs, not a custom tool. Notable
behavior that isn't obvious from the on-disk format alone (see
pkg/romfs/build.go):
- Only the root directory gets explicit
./..entries synthesized as a special case;.is a real directory entry (self-referencing),..is a hard link (type 0) to., both always first. - Every other directory's
./..are ordinary hard-link entries —.points to that directory's own header offset,..to the parent directory's header offset — and they appear at whatever positionreaddir()returned them on the original build machine, not necessarily first.panagocaptures their exact position per directory (same order sidecar as cramfs) and replays them as hard links at that position. - Directory entries carry the executable bit (traversable), including the
root
.entry;..never does. - A directory's own recursive content is written immediately inline (unlike cramfs's two-pass layout) — a large early subdirectory pushes every later sibling's offset out by its full size.
- The header checksum covers the 16-byte header plus the name padded to
16 bytes — not just the header, despite
romfs.txtheader diagrams suggesting otherwise.
Several "size" fields throughout this format are placeholders or are otherwise short of the real physical boundary, rather than authoritative:
- The MAIN module-table entry's declared size can be dozens of bytes short
of where MAIN's data actually ends (there's slack before the next
partition starts). Both
firmware decodeandfirmware encodetreat the declared size as a lower bound, not a hard limit — they read/write up to the next partition's real offset, and only error if content would overflow that true boundary. - The cramfs superblock's "total size" field is a fixed placeholder
(
0x00010000) on every real image, unrelated to the actual image size. - The romfs superblock's declared size is the real content size before its own trailing 1024-byte alignment pad — the on-disk file is a few dozen bytes larger than the field says.
- Each filesystem partition (fma5/fma6/fma7) is allocated a fixed size in
the firmware — matching the real NAND partition table, not the actual
filesystem content — padded with a small zero-aligned gap (4096-byte
boundary for cramfs, matching its own
BLOCK_SIZE; 1024-byte for romfs, matchinggenromfs's own end-of-image padding) followed by0xFFfill (the conventional erased-flash byte) out to the fixed allocation.
panago-cli extract followed immediately by panago-cli build (no
modifications) now reproduces the original firmware exactly, verified
by SHA-256 match against a real PANAEUSB.FRM. This required all of the
above — reproducing not just each filesystem's logical content, but every
placeholder field, padding convention, and reserved NAND allocation the
original firmware happened to contain. In particular:
firmware buildderives each sub-partition's true fixed size from the template firmware (viafirmware.GetTemplatePartitionSizes), not a hardcoded constant — so it stays correct across firmware versions/models with different partition tables — and pads the freshly rebuilt cramfs/romfs images out to that size (seefirmware.PadPartitionToSize).- Modifying content and rebuilding works the same way: as long as the
modified filesystem still fits within the original's fixed partition
allocation, the rebuilt firmware keeps the exact same MAIN chunk layout as
the original (same chunk count, same chunk boundaries), which is also why
BufferConstantlookups almost always find an exact match even for modified content — see the MAIN Partition Structure section above. - If modified content no longer fits — either the filesystem itself exceeds
its fixed NAND allocation, or the LZSS-compressed MAIN data would exceed
the true space before the next partition —
buildfails with a clear error rather than silently truncating or corrupting output.
The device has devpts kernel support but it is not mounted by default. To enable interactive shells (e.g. via dropbear SSH):
mkdir -p /dev/pts
mount -t devpts devpts /dev/ptsAfter this, PTY allocation works correctly and interactive terminal sessions are fully functional.
- Go 1.21 or later
make build # Build all binaries (panago + panago-cli)
make test # Run tests
make clean # Clean build artifacts# For ARM devices
GOOS=linux GOARCH=arm go build -o panago-arm ./cmd/cli# Local test build
goreleaser release --snapshot --skip=publish --clean
# Tag and push to trigger official release
git tag v0.1.2
git push origin v0.1.2Firmware integration tests require a real firmware file. Set the path via environment variable or place the file at the default location:
# Use a specific firmware file
PANAGO_TEST_FIRMWARE=/path/to/PANAEUSB.FRM go test ./pkg/firmware/
# Run only unit tests (no firmware required)
go test ./pkg/firmware/ -run TestLZSSSee LICENSE file.