Binwalk

Binwalk scans a file for known magic-byte signatures and tells you what is packed inside it: compressed blobs, filesystems, bootloaders, certificates, and appended files. Reach for it the moment you are handed a firmware dump or a suspicious binary and need a map before you carve, because almost every next step depends on knowing what the file contains and where each piece begins.

Mental model: binwalk [action] [options] TARGET_FILE. The default action is a signature scan; -e extracts, -E graphs entropy, everything else is one flag away.

New to firmware? Core concepts
  • Signature (magic bytes): a byte pattern that names a known format; the default binwalk TARGET_FILE is a signature scan
  • Offset: where a detected item starts, shown in DECIMAL and HEXADECIMAL; you carve by offset later
  • Extraction (-e): pulls detected items into _TARGET_FILE.extracted/ using matching unpackers
  • Recursive / Matryoshka (-M): keeps unpacking what it just extracted, firmware blob down to filesystem
  • Entropy (-E): how random the bytes are; flat-high means compression or encryption, flat-low means padding
  • Carving: copying a byte range out into its own file (dd by hand, or -D by rule)
  • Filesystem: SquashFS, JFFS2, cramfs, UBI, the root filesystem and usually the prize in firmware
  • Unpacker: the external helper (unsquashfs, mtd-utils) Binwalk shells out to so -e can actually extract
When to reach for Binwalk (and when not)

Reach for it to map an unknown firmware image, find data appended or embedded in a CTF artifact, confirm what a packed or self-extracting binary contains, or unpack your own router image to read its filesystem and configs. It surfaces structure so you can carve it out; it does not exploit anything.

Reach for something else when you already know the format (use unzip, tar, unsquashfs directly), when the hidden data is image or audio steganography (steghide, zsteg), when a region is encrypted rather than compressed (Binwalk finds it but cannot decrypt it), or when it stalls on a deeply nested container and you want a more aggressive recursive unpacker (unblob).

Install, update, verify#

sudo apt install -y binwalk               # Debian / Ubuntu / Kali
sudo dnf install -y binwalk               # Fedora / RHEL
pipx install binwalk                      # pip route (older 2.x)

# Extraction helpers, so -e can actually unpack what it detects
sudo apt install -y mtd-utils squashfs-tools p7zip-full \
  zstd lzop unrar cabextract sleuthkit

sudo apt install --only-upgrade -y binwalk   # update

binwalk --help          # version sits in the banner (2.x); 3.x: binwalk --version
command -v binwalk      # confirm it is on PATH

Self-test without touching anything external: printf 'hello ' > t; echo data | gzip >> t; binwalk t should print a DECIMAL HEXADECIMAL DESCRIPTION table with a gzip compressed data row. If a filesystem detects but never extracts later, you are missing one of the helper unpackers above.

Flags you’ll actually use#

Skim these first; the cheat sheet below is these flags combined.

FlagDoesFlagDoes
-eExtract known types to _file.extracted/-yOnly show/extract matching signatures
-MRecurse into extracted files (Matryoshka)-xExclude matching signatures
-d NCap -M recursion depth-AScan for executable opcode signatures
-C DIRExtract into DIR, not beside the file-YIdentify CPU arch (capstone disasm)
-rDelete carved files after extraction-EEntropy graph (compression vs encryption)
-zCarve regions, skip external extractors-JSave the entropy plot as a PNG
-D / --ddCarve type:ext[:cmd] yourself-o / -lStart offset / bytes to scan
-RScan for a raw byte sequence-tFit the table to the terminal width
-f / -cLog to a file / as CSV-v / -qVerbose / quiet
-BSignature scan (the default)-hHelp and version banner

Cheat sheet#

Every block starts simple and adds one flag at a time, so you can stop at the line that does the job.

# Build up a scan: stop at the line you need
binwalk TARGET_FILE                       # 1. signature map: what is inside
binwalk -t TARGET_FILE                    # 2. + fit the table to your terminal
binwalk -y filesystem TARGET_FILE         # 3. + only filesystem rows
binwalk -x lzma -x zlib TARGET_FILE       # 4. or drop noisy false-positive classes

# Extract
binwalk -e TARGET_FILE                    # extract into _TARGET_FILE.extracted/
binwalk -e -C extracted TARGET_FILE       # extract into a tidy directory
binwalk -Me TARGET_FILE                   # recursive: unpack firmware layers
binwalk -Me -d 4 TARGET_FILE              # cap recursion depth at 4 levels
binwalk -re TARGET_FILE                   # extract, then delete the carved blobs

# Carve by hand or by rule
binwalk TARGET_FILE | grep -i squashfs    # find the part, note the DECIMAL offset
dd if=TARGET_FILE of=fs.bin bs=1 skip=OFFSET 2>/dev/null   # carve from that offset
binwalk -D 'squashfs:squashfs' TARGET_FILE  # rule-based carve to a .squashfs file
binwalk -z TARGET_FILE                    # carve regions, but do not run extractors

# Analyze without extracting
binwalk -E TARGET_FILE                    # entropy graph (needs a display)
binwalk -E -J TARGET_FILE                 # entropy, saved as a PNG
binwalk -A TARGET_FILE                    # find executable opcodes in a raw blob
binwalk -Y TARGET_FILE                    # guess CPU architecture (ARM/MIPS/...)

# Scope the scan to part of a big file
binwalk -o 1048576 TARGET_FILE            # start scanning at byte 1048576
binwalk -o 1048576 -l 65536 TARGET_FILE   # scan a 64 KB window from that offset
binwalk -R '\x1f\x8b\x08' TARGET_FILE     # hunt a raw byte sequence (gzip magic)

# Record what you found
binwalk -f scan.log TARGET_FILE           # log the table to a file
binwalk -f scan.csv -c TARGET_FILE        # log the table as CSV (diffable)
binwalk TARGET_FILE | tee notes/scan.txt  # save the human-readable table
sha256sum TARGET_FILE | tee notes/hash.txt  # hash the original before you touch it

Command breakdowns#

Each block below explains one situation, the exact command, and what to expect back.

Map an unknown file with a signature scan#

binwalk TARGET_FILE

Prints a DECIMAL HEXADECIMAL DESCRIPTION table, one row per recognized signature. This is your map of the file: every later decision reads off these offsets and format names.

Extract everything Binwalk recognizes#

binwalk -e TARGET_FILE

Unpacks each detected item into _TARGET_FILE.extracted/ using matching helpers. Inspect it with ls -R _*.extracted. If a format detects but no folder appears, the unpacker for it is not installed.

Unpack a firmware image layer by layer#

binwalk -Me TARGET_FILE

-M (Matryoshka) re-runs extraction on everything it just pulled out, so a firmware blob unwraps through its compression layers down to the root filesystem. This is the standard firmware command.

Keep recursion from exploding on a huge image#

binwalk -Me -d 4 -C extracted TARGET_FILE

-d 4 caps recursion at four levels and -C sends output to extracted/ instead of beside the file. Use both when -Me on a large image threatens to fill the disk with nested folders.

Extract into a tidy directory#

binwalk -e -C extracted TARGET_FILE

-C keeps every _*.extracted tree under one directory you chose, so output never clobbers your workspace or buries the original next to a pile of carved blobs.

Show only the filesystem rows#

binwalk -y filesystem TARGET_FILE

-y filters the scan to signatures whose description matches filesystem, so SquashFS, JFFS2, cramfs, and UBI rows surface without the gzip and lzma noise. The fast way to answer “is the rootfs in here?”

Cut noisy false-positive signatures#

binwalk -x lzma -x zlib TARGET_FILE

-x drops rows whose description matches the string, one -x per class. Deep lzma or zlib “matches” inside random data are usually false; excluding them makes the real structure readable.

Carve a byte range out by hand#

binwalk TARGET_FILE                 # note the DECIMAL offset of the part you want
dd if=TARGET_FILE of=blob.bin bs=1 skip=OFFSET 2>/dev/null

When -e refuses to unpack a region, carve it yourself: dd copies from skip=OFFSET to the end of the file. Reliable and boring, which is exactly what you want on a stubborn blob.

Carve by rule instead of by hand#

binwalk -D 'squashfs:squashfs' TARGET_FILE

-D type:ext carves every region matching the type signature into files with the given extension, no manual dd. Add a third :cmd field to run a command on each carved file as it lands.

Spot compressed or encrypted regions with entropy#

binwalk -E -J TARGET_FILE

-E measures byte randomness; -J saves the plot as a PNG (drop -J if you have a display). Flat-high entropy means compression or encryption, flat-low means padding, and a sudden rise marks where a compressed blob begins.

Identify the CPU architecture of a raw blob#

binwalk -Y TARGET_FILE

-Y runs the capstone disassembler and reports the architecture (ARM, MIPS, and so on) it decodes cleanly. Run it before loading a headerless firmware blob into a disassembler so you pick the right CPU. -A is the lighter opcode-signature version.

Scan only part of a large file#

binwalk -o 1048576 -l 65536 TARGET_FILE

-o starts the scan at a byte offset and -l limits how many bytes to read, so you re-scan a 64 KB window around an interesting offset instead of re-processing a multi-gigabyte image.

Hunt a specific byte sequence#

binwalk -R '\x1f\x8b\x08' TARGET_FILE

-R scans for a raw byte string instead of the signature database. Here it finds every gzip header (1f 8b 08); use it when you know the exact magic you are chasing and want the offsets fast.

Save the scan for your notes#

binwalk -f scan.csv -c TARGET_FILE
sha256sum TARGET_FILE | tee notes/hash.txt

-f logs the table to a file and -c makes that log CSV, which is easy to diff between firmware versions. Hash the original first so you can prove exactly what you analyzed.

Reading the output#

You seeMeaningDo next
Squashfs filesystem / JFFS2 / UBIThe root filesystemThe prize; -Me extracts it, then read the tree
gzip / xz / LZMA compressed dataA compressed blob-e will try to unpack it; note the offset
uImage header / U-BootBootloader or kernel imageThe kernel sits right after; carve from the offset
A lone signature deep in random bytesLikely a false positiveVerify with file, or -x it out; do not trust blindly
High entropy, no signaturesProbably encryptedBinwalk finds it but cannot open it; move on
Empty tablePlain file, encrypted, or unknown formatTry -E, update Binwalk, or check file

The DECIMAL and HEXADECIMAL columns are the same offset in two bases (feed either to dd’s skip= or to -o); DESCRIPTION names the format and often its size and parameters.

Troubleshooting#

ProblemCauseFix
command not foundNot installed / stale PATHReinstall; open a fresh shell
Detects a filesystem but will not extractMissing unpackerInstall squashfs-tools, mtd-utils, p7zip-full
-e produces empty or junk foldersNo matching helper, or false matchesAdd unpackers; carve by offset with dd
Empty scan tableEncrypted or unknown formatTry -E for entropy; update Binwalk for new signatures
Extraction fills the disk-M recursing a huge imageAdd -d to cap depth and -C to a big partition
Permission denied writing outputRead-only or root-owned dirRun from a writable dir, or set -C
-E shows nothing / errorsNo display for the plotAdd -J to save a PNG instead
Commands differ from a tutorial2.x Python vs 3.x RustCheck binwalk --help banner; flags and output changed
Garbled or partial extracted filesTruncated or corrupt sourceRe-hash the original; carve the range by offset

Gotchas#

  • A signature match is a hint, not proof. A lone row deep inside otherwise-random bytes is usually a false positive; confirm with file, strings, or the surrounding structure before you trust it.
  • -e without -M stops at the first layer. Firmware unwraps in stages, so a plain -e hands you one compressed blob and you conclude “nothing here” when the filesystem was one recursion away.
  • A detected filesystem still needs its unpacker installed. Binwalk recognizes SquashFS from its own signature database but shells out to unsquashfs and mtd-utils to extract it; without them you get a row and no files.
  • High entropy means “cannot compress further,” not “compressed.” Encryption and compression both read as flat-high on the -E graph, so entropy alone will not tell you which; never promise decryption from a graph.
  • Binwalk 3.x is a different tool under the same name. The Rust rewrite changed flags, extractor config, and output, so a command copied from a 2.x guide can silently do something else. Run binwalk --help first to see which one you have.
  • Recursive extraction on an untrusted image can behave like a zip bomb. -Me will happily unpack layer after layer and fill the disk; cap it with -d and give it its own -C directory on a partition you can afford to lose.

Pairs with#

file, strings, and xxd confirm and read what a scan points at before you trust the label. dd carves the exact byte range from an offset Binwalk reports, and unsquashfs or mount opens the filesystem once it is out. When the hidden data is image or audio steganography rather than an embedded format, reach for steghide or zsteg; when Binwalk stalls on a deeply nested container, unblob is the stronger recursive unpacker, and foremost or scalpel give a second opinion on pure file carving.

Find us elsewhere

Merch, stickers, and moreSupport the work at the Solvere Labs shop