rea-reverse

Extract SquashFS from Firmware .bin: unsquashfs, Unblob, Binwalk

Written for REA 6.4.0Updated 8 min read
On this page

Short answer: most Linux-based router firmware is a kernel followed by a SquashFS root filesystem. Find the SquashFS magic hsqs (68 73 71 73, little-endian) with binwalk, copy the bytes from that offset with dd, and run unsquashfs on the result. If you would rather not count bytes, binwalk -e and unblob do the same in one step, but both call an external SquashFS extractor (sasquatch) that you have to install. Everything below was run on a real OpenWrt image, with the outputs shown as they came out.

Who this is for: developers and home-lab users who have a firmware .bin for their own device, or an open-source build, and want to read its files: config defaults, startup scripts, the BusyBox version, the web server binary.

What you will do:

  1. Download an OpenWrt sysupgrade image and verify its checksum.
  2. Locate the SquashFS region and read its superblock.
  3. Carve it with dd and unpack it with unsquashfs.
  4. Do the same with Binwalk 3, Unblob and REA, and see where each one stops.
  5. Check the first things worth looking at in the extracted tree.

Only unpack firmware you are allowed to examine: images for devices you own, open-source builds such as OpenWrt, or CTF targets. Extraction here is static: nothing is flashed, mounted or run. See REA’s DISCLAIMER.md.

The sample: an OpenWrt sysupgrade image

I used the OpenWrt 25.12.5 image for the TP-Link Archer C7 v5, a common MIPS router. OpenWrt publishes a sha256sums file next to every target, so check the download before doing anything else:

$ curl -LO https://downloads.openwrt.org/releases/25.12.5/targets/ath79/generic/openwrt-25.12.5-ath79-generic-tplink_archer-c7-v5-squashfs-sysupgrade.bin
$ curl -LO https://downloads.openwrt.org/releases/25.12.5/targets/ath79/generic/sha256sums
$ grep c7-v5-squashfs-sysupgrade sha256sums
f15b16b36df7fce8a5cf1302320fdb428c3bb6fbc2949a4858afa257f4f364a2 *openwrt-25.12.5-ath79-generic-tplink_archer-c7-v5-squashfs-sysupgrade.bin
$ shasum -a 256 openwrt-25.12.5-ath79-generic-tplink_archer-c7-v5-squashfs-sysupgrade.bin
f15b16b36df7fce8a5cf1302320fdb428c3bb6fbc2949a4858afa257f4f364a2  openwrt-25.12.5-...-sysupgrade.bin

The file is 7,405,862 bytes. I’ll call it fw.bin from here on (cp it to that name if you want to paste the commands). file only describes the first header it sees:

$ file fw.bin
fw.bin: u-boot legacy uImage, MIPS OpenWrt Linux-6.12.94, Linux/MIPS, OS Kernel Image (lzma), 2741921 bytes, ...

That is accurate but incomplete. The uImage header covers the kernel only, and the root filesystem follows it. If you are not sure what kind of file you have, Identify an Unknown File Type by Its Magic Bytes covers that first step.

Step 1: Find the SquashFS offset

A signature scanner reads the whole file and reports every header it recognizes. Binwalk 3.1.0 (installed from Homebrew on macOS here, see the Binwalk 3 guide) finds two regions:

$ binwalk fw.bin
DECIMAL     HEXADECIMAL   DESCRIPTION
0           0x0           uImage firmware image, header size: 64 bytes, data size: 2741921 bytes,
                          compression: lzma, CPU: MIPS32, OS: Linux, image type: OS Kernel Image, ...
2741985     0x29D6E1      SquashFS file system, little endian, version: 4.0, compression: xz,
                          inode count: 1214, block size: 262144, image size: 4626494 bytes, ...

(I trimmed the column padding.) Two numbers matter: the offset, 2,741,985, and the image size, 4,626,494 bytes. The offset is exactly the 64-byte uImage header plus the 2,741,921-byte kernel, which is a good sign that the parse is right.

You don’t need Binwalk to confirm it. SquashFS 4 starts with hsqs on little-endian images (sqsh on big-endian ones), and the superblock fields follow:

$ xxd -s 2741985 -l 32 fw.bin
0029d6e1: 6873 7173 be04 0000 286c 426a 0000 0400  hsqs....(lBj....
0029d6f1: 2200 0000 0400 1200 c002 0100 0400 0000  "...............

$ LC_ALL=C grep -obUa hsqs fw.bin
2741985:hsqs

be04 0000 is the inode count (0x4be = 1214) and 0000 0400 is the block size (0x40000 = 262,144), matching Binwalk. The grep shows the magic appears only once, so there is no second filesystem hiding further in.

Step 2: Carve the region with dd

Copy exactly the SquashFS bytes into their own file. dd with bs=1 is easy to read, but it copies one byte at a time (about 8 seconds for 4.6 MB on my Mac). tail/head gives the same bytes much faster:

dd if=fw.bin of=rootfs.squashfs bs=1 skip=2741985 count=4626494
# same bytes, faster:
tail -c +2741986 fw.bin | head -c 4626494 > rootfs.squashfs

tail -c +N is 1-based, hence the +1. Both produced a file with SHA-256 23003e29…41095bd, and REA reported the same digest for its own selection of that range (Step 5).

Why give a length at all: the bytes after the filesystem are not part of it. In this image they are 0xFF padding and a small JSON block with the device metadata ("supported_devices":["tplink,archer-c7-v5","archer-c7-v5"] and the OpenWrt version), which sysupgrade checks before flashing. unsquashfs tolerated the extra tail when I left count off, but a clean carve is easier to hash and compare.

Step 3: Unpack with unsquashfs

unsquashfs comes with squashfs-tools (apt install squashfs-tools on Debian and Ubuntu, brew install squashfs on macOS; “unsquashfs: command not found” means this package is missing). Read the superblock before you extract:

$ unsquashfs -s rootfs.squashfs
Found a valid SQUASHFS 4:0 superblock on rootfs.squashfs.
Creation or last append time Mon Jun 29 20:59:20 2026
Filesystem size 4626494 bytes (4518.06 Kbytes / 4.41 Mbytes)
Compression xz
Block size 262144
...
Number of fragments 34
Number of inodes 1214
Number of ids 1

The compression line matters. Stock squashfs-tools handles gzip, xz, lzo, lz4 and zstd if it was built with them, but some vendors use LZMA variants or modified headers that stock tools refuse (see Step 6). Now extract into a fresh directory:

$ unsquashfs -d rootfs rootfs.squashfs
create_inode: could not create character device rootfs/dev/console, because you're not superuser!
...
created 870 files
created 129 directories
created 214 symlinks
created 0 devices

The device-node warning is expected for a normal user and harmless for static reading. unsquashfs -ll rootfs.squashfs lists the contents with modes and owners without writing anything, which is useful when you only want to search for a path.

Step 4: Let Binwalk or Unblob do it

Both tools find the region and carve it for you, but for SquashFS they hand the work to sasquatch, a patched unsquashfs. On my Mac, where sasquatch is not packaged (its source build needs the Linux-only header sys/sysinfo.h), Binwalk 3 extracted the kernel and failed on the filesystem:

$ binwalk -e -C bw fw.bin
ERROR binwalk::extractors::common] Failed to execute command sasquatch[".../bw/fw.bin.extracted/29D6E1/squashfs_29D6E1.sqsh"]: No such file or directory (os error 2)
[+] Extraction of uimage data at offset 0x0 completed successfully
[-] Extraction of squashfs data at offset 0x29D6E1 failed!

So I moved to Linux: a Debian 13 arm64 container (Podman on macOS) with Binwalk 3.1.0 built by cargo install binwalk --version 3.1.0, Unblob 26.6.4 from PyPI, and the sasquatch_1.0_arm64.deb from onekey-sec’s sasquatch release v4.5.1-6, the install command both projects document. With sasquatch on PATH, both tools worked on the same file:

$ binwalk -e -C bw openwrt-...-sysupgrade.bin
[+] Extraction of uimage data at offset 0x0 completed successfully
[+] Extraction of squashfs data at offset 0x29D6E1 completed successfully
$ ls bw/openwrt-...-sysupgrade.bin.extracted/29D6E1/squashfs-root
bin  dev  etc  init  lib  mnt  overlay  proc  rom  root ...

$ unblob -e unb openwrt-...-sysupgrade.bin
$ ls unb/openwrt-...-sysupgrade.bin_extract/
0-64.unknown  2741985-7368479.squashfs_v4_le_extract  64-2741985.lzma_extract  7368479-7405862.unknown

Binwalk names output directories by hex offset (29D6E1), while Unblob uses decimal start and end offsets (2741985-7368479). Unblob also writes out the bytes it could not identify as .unknown chunks: here the 64-byte uImage header and the 37,383-byte tail. Its summary reported a 66.37% chunk identification ratio for the whole run. The Binwalk 3 guide compares the two tools in more detail.

Step 5: The same with REA

REA, the open-source project by morluto, wraps the Binwalk and Unblob executables you supply and returns structured Evidence instead of a terminal table. In 6.4.0 the two firmware commands are Linux-only. On macOS they refuse immediately:

{"code":"capability_unavailable","message":"This integration requires Linux and an absolute REA_BINWALK_COMMAND path to the caller-supplied tool", ...}

In the Linux container I installed [email protected] from the npm tarball, pointed it at the tools with absolute paths, and ran:

export REA_BINWALK_COMMAND=/usr/local/cargo/bin/binwalk
export REA_UNBLOB_COMMAND=/opt/unblob/bin/unblob
rea inspect-firmware-regions /root/fw/openwrt-...-sysupgrade.bin --format json
rea extract-firmware /root/fw/openwrt-...-sysupgrade.bin /root/fw/sq --offset 2741985 --length 4626494 --format json

inspect-firmware-regions returned the same two regions with reported_size 2,741,985 and 4,626,494, each tagged size_basis: provider_reported_validation_unknown, a reminder that a reported size is not a validated partition boundary. extract-firmware with that range returned coverage: "partial", 1,186 regular files, 214 unpublished_entries (the BusyBox symlinks, recorded with their link_target but not written to disk), one depth-limited path and seven diagnostics, all “Duplicate tar entry.” inside OpenWrt’s package database. The selection digest was 23003e29…41095bd, the same as my dd carve, and the extracted bin/busybox had the same SHA-256 (458f75d0…db585d5e3) as the one unsquashfs produced on the Mac.

Three things tripped me up, and you are likely to hit them too:

  • REA needs a procps-compatible ps on its PATH on Linux. The slim Debian image had none, and both commands failed with “Linux process ownership inspection requires a procps-compatible ps on REA’s PATH” until I installed procps.
  • My first attempts read and wrote a folder shared from macOS into the VM. Those failed with cleanup errors (“Extraction directory identity changed”). Copying the image into the container’s own filesystem fixed it.
  • The output directory must not exist. A second run into the same directory returned “Extraction output root already exists”.

REA’s docs list SquashFS as an unverified format and its verification lane is Linux x64; I ran on Linux arm64, which the 6.4.0 code accepts (it only checks for Linux). The workflow guide Firmware Analysis with REA: Binwalk, Unblob and Ghidra covers budgets, depth and the Ghidra handoff.

Step 6: When the standard tools refuse

Some vendors change the SquashFS magic, the superblock layout or the LZMA parameters. To see what that looks like, I overwrote the first four bytes of my own carved copy with abcd:

$ unsquashfs -s patched.sqsh
FATAL ERROR: Can't find a valid SQUASHFS superblock on patched.sqsh
$ binwalk patched.sqsh
4626120     0x4696C8      XZ compressed data, total size: 344 bytes

Binwalk no longer reports a filesystem at all, only a stray XZ stream near the end. That is the signal to try sasquatch. Its original README describes it as patches to unsquashfs that try to support “as many hacked-up vendor-specific SquashFS implementations as possible”, including brute-forcing changed LZMA options. I did not test it on a real vendor image, so treat it as the next thing to try rather than a guaranteed fix. If the region has high entropy and no recognizable header, it may be encrypted, and no unpacker will help.

Not every image is SquashFS. Binwalk 3’s own extractor list (binwalk -L) maps the common alternatives like this:

Filesystem Binwalk 3 calls Notes
SquashFS sasquatch Plain unsquashfs works for standard images
JFFS2 jefferson Common on NOR flash
UBI / UBIFS ubireader_extract_images / ubireader_extract_files NAND devices; from the ubi_reader project
CramFS 7zz Older devices

Unblob uses the same helpers; unblob --show-external-dependencies prints which ones it can find.

Step 7: What to look at first

Start with the identity files, then the binaries:

$ cat rootfs/etc/openwrt_release
DISTRIB_RELEASE='25.12.5'
DISTRIB_REVISION='r33051-f5dae5ece4'
DISTRIB_TARGET='ath79/generic'
DISTRIB_ARCH='mips_24kc'
$ strings rootfs/bin/busybox | grep -m1 "BusyBox v"
BusyBox v1.37.0 (2026-06-29 12:59:20 UTC)
$ file rootfs/bin/busybox
rootfs/bin/busybox: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-mips-sf.so.1, no section header

/etc holds defaults and init scripts, /usr/sbin the daemons (uhttpd, dnsmasq, dropbear, hostapd here). To read a binary’s headers without a toolchain, drop it into the native binary overview. For busybox it showed ELF32, MIPS, big-endian, ET_EXEC, the musl interpreter, the needed libraries libgcc_s.so.1 and libc.so, and stripped: yes (no section headers). uhttpd came out as a PIE (ET_DYN, DF_1_PIE) linked against libubox and json-c. The viewer runs in your browser and does not upload the file. ELF, PE and Mach-O Viewer Online explains each field.

FAQ

Why does file only say “uImage”? file describes the header at offset 0 and stops. Use a scanner such as Binwalk, or search for hsqs, to find what follows.

Do I need root? No. Only device nodes such as /dev/console need root, and you don’t need them for reading files.

Does this work on Windows? squashfs-tools and sasquatch are Linux tools. Use WSL or a Linux VM. On macOS, Homebrew’s squashfs gives you unsquashfs, which handled this standard image fine.

Is the extracted tree the same as the running device? No. It is the read-only base. On OpenWrt, changes live in an overlay partition that the image does not contain.

Authorised use: extract firmware only from devices you own, open-source projects or CTF challenges, and follow each vendor’s license terms.

Next steps